motion.rs

   1use editor::{
   2    Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
   3    display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
   4    movement::{
   5        self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
   6    },
   7};
   8use gpui::{Action, Context, Window, actions, px};
   9use language::{CharKind, Point, Selection, SelectionGoal};
  10use multi_buffer::MultiBufferRow;
  11use schemars::JsonSchema;
  12use serde::Deserialize;
  13use std::ops::Range;
  14use workspace::searchable::Direction;
  15
  16use crate::{
  17    Vim,
  18    normal::mark,
  19    state::{Mode, Operator},
  20    surrounds::SurroundsType,
  21};
  22
  23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
  24pub(crate) enum MotionKind {
  25    Linewise,
  26    Exclusive,
  27    Inclusive,
  28}
  29
  30impl MotionKind {
  31    pub(crate) fn for_mode(mode: Mode) -> Self {
  32        match mode {
  33            Mode::VisualLine => MotionKind::Linewise,
  34            _ => MotionKind::Exclusive,
  35        }
  36    }
  37
  38    pub(crate) fn linewise(&self) -> bool {
  39        matches!(self, MotionKind::Linewise)
  40    }
  41}
  42
  43#[derive(Clone, Debug, PartialEq, Eq)]
  44pub enum Motion {
  45    Left,
  46    WrappingLeft,
  47    Down {
  48        display_lines: bool,
  49    },
  50    Up {
  51        display_lines: bool,
  52    },
  53    Right,
  54    WrappingRight,
  55    NextWordStart {
  56        ignore_punctuation: bool,
  57    },
  58    NextWordEnd {
  59        ignore_punctuation: bool,
  60    },
  61    PreviousWordStart {
  62        ignore_punctuation: bool,
  63    },
  64    PreviousWordEnd {
  65        ignore_punctuation: bool,
  66    },
  67    NextSubwordStart {
  68        ignore_punctuation: bool,
  69    },
  70    NextSubwordEnd {
  71        ignore_punctuation: bool,
  72    },
  73    PreviousSubwordStart {
  74        ignore_punctuation: bool,
  75    },
  76    PreviousSubwordEnd {
  77        ignore_punctuation: bool,
  78    },
  79    FirstNonWhitespace {
  80        display_lines: bool,
  81    },
  82    CurrentLine,
  83    StartOfLine {
  84        display_lines: bool,
  85    },
  86    MiddleOfLine {
  87        display_lines: bool,
  88    },
  89    EndOfLine {
  90        display_lines: bool,
  91    },
  92    SentenceBackward,
  93    SentenceForward,
  94    StartOfParagraph,
  95    EndOfParagraph,
  96    StartOfDocument,
  97    EndOfDocument,
  98    Matching,
  99    GoToPercentage,
 100    UnmatchedForward {
 101        char: char,
 102    },
 103    UnmatchedBackward {
 104        char: char,
 105    },
 106    FindForward {
 107        before: bool,
 108        char: char,
 109        mode: FindRange,
 110        smartcase: bool,
 111    },
 112    FindBackward {
 113        after: bool,
 114        char: char,
 115        mode: FindRange,
 116        smartcase: bool,
 117    },
 118    Sneak {
 119        first_char: char,
 120        second_char: char,
 121        smartcase: bool,
 122    },
 123    SneakBackward {
 124        first_char: char,
 125        second_char: char,
 126        smartcase: bool,
 127    },
 128    RepeatFind {
 129        last_find: Box<Motion>,
 130    },
 131    RepeatFindReversed {
 132        last_find: Box<Motion>,
 133    },
 134    NextLineStart,
 135    PreviousLineStart,
 136    StartOfLineDownward,
 137    EndOfLineDownward,
 138    GoToColumn,
 139    WindowTop,
 140    WindowMiddle,
 141    WindowBottom,
 142    NextSectionStart,
 143    NextSectionEnd,
 144    PreviousSectionStart,
 145    PreviousSectionEnd,
 146    NextMethodStart,
 147    NextMethodEnd,
 148    PreviousMethodStart,
 149    PreviousMethodEnd,
 150    NextComment,
 151    PreviousComment,
 152    PreviousLesserIndent,
 153    PreviousGreaterIndent,
 154    PreviousSameIndent,
 155    NextLesserIndent,
 156    NextGreaterIndent,
 157    NextSameIndent,
 158
 159    // we don't have a good way to run a search synchronously, so
 160    // we handle search motions by running the search async and then
 161    // calling back into motion with this
 162    ZedSearchResult {
 163        prior_selections: Vec<Range<Anchor>>,
 164        new_selections: Vec<Range<Anchor>>,
 165    },
 166    Jump {
 167        anchor: Anchor,
 168        line: bool,
 169    },
 170}
 171
 172#[derive(Clone, Copy)]
 173enum IndentType {
 174    Lesser,
 175    Greater,
 176    Same,
 177}
 178
 179/// Moves to the start of the next word.
 180#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 181#[action(namespace = vim)]
 182#[serde(deny_unknown_fields)]
 183struct NextWordStart {
 184    #[serde(default)]
 185    ignore_punctuation: bool,
 186}
 187
 188/// Moves to the end of the next word.
 189#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 190#[action(namespace = vim)]
 191#[serde(deny_unknown_fields)]
 192struct NextWordEnd {
 193    #[serde(default)]
 194    ignore_punctuation: bool,
 195}
 196
 197/// Moves to the start of the previous word.
 198#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 199#[action(namespace = vim)]
 200#[serde(deny_unknown_fields)]
 201struct PreviousWordStart {
 202    #[serde(default)]
 203    ignore_punctuation: bool,
 204}
 205
 206/// Moves to the end of the previous word.
 207#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 208#[action(namespace = vim)]
 209#[serde(deny_unknown_fields)]
 210struct PreviousWordEnd {
 211    #[serde(default)]
 212    ignore_punctuation: bool,
 213}
 214
 215/// Moves to the start of the next subword.
 216#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 217#[action(namespace = vim)]
 218#[serde(deny_unknown_fields)]
 219pub(crate) struct NextSubwordStart {
 220    #[serde(default)]
 221    pub(crate) ignore_punctuation: bool,
 222}
 223
 224/// Moves to the end of the next subword.
 225#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 226#[action(namespace = vim)]
 227#[serde(deny_unknown_fields)]
 228pub(crate) struct NextSubwordEnd {
 229    #[serde(default)]
 230    pub(crate) ignore_punctuation: bool,
 231}
 232
 233/// Moves to the start of the previous subword.
 234#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 235#[action(namespace = vim)]
 236#[serde(deny_unknown_fields)]
 237pub(crate) struct PreviousSubwordStart {
 238    #[serde(default)]
 239    pub(crate) ignore_punctuation: bool,
 240}
 241
 242/// Moves to the end of the previous subword.
 243#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 244#[action(namespace = vim)]
 245#[serde(deny_unknown_fields)]
 246pub(crate) struct PreviousSubwordEnd {
 247    #[serde(default)]
 248    pub(crate) ignore_punctuation: bool,
 249}
 250
 251/// Moves cursor up by the specified number of lines.
 252#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 253#[action(namespace = vim)]
 254#[serde(deny_unknown_fields)]
 255pub(crate) struct Up {
 256    #[serde(default)]
 257    pub(crate) display_lines: bool,
 258}
 259
 260/// Moves cursor down by the specified number of lines.
 261#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 262#[action(namespace = vim)]
 263#[serde(deny_unknown_fields)]
 264pub(crate) struct Down {
 265    #[serde(default)]
 266    pub(crate) display_lines: bool,
 267}
 268
 269/// Moves to the first non-whitespace character on the current line.
 270#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 271#[action(namespace = vim)]
 272#[serde(deny_unknown_fields)]
 273struct FirstNonWhitespace {
 274    #[serde(default)]
 275    display_lines: bool,
 276}
 277
 278/// Moves to the end of the current line.
 279#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 280#[action(namespace = vim)]
 281#[serde(deny_unknown_fields)]
 282struct EndOfLine {
 283    #[serde(default)]
 284    display_lines: bool,
 285}
 286
 287/// Moves to the start of the current line.
 288#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 289#[action(namespace = vim)]
 290#[serde(deny_unknown_fields)]
 291pub struct StartOfLine {
 292    #[serde(default)]
 293    pub(crate) display_lines: bool,
 294}
 295
 296/// Moves to the middle of the current line.
 297#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 298#[action(namespace = vim)]
 299#[serde(deny_unknown_fields)]
 300struct MiddleOfLine {
 301    #[serde(default)]
 302    display_lines: bool,
 303}
 304
 305/// Finds the next unmatched bracket or delimiter.
 306#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 307#[action(namespace = vim)]
 308#[serde(deny_unknown_fields)]
 309struct UnmatchedForward {
 310    #[serde(default)]
 311    char: char,
 312}
 313
 314/// Finds the previous unmatched bracket or delimiter.
 315#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 316#[action(namespace = vim)]
 317#[serde(deny_unknown_fields)]
 318struct UnmatchedBackward {
 319    #[serde(default)]
 320    char: char,
 321}
 322
 323actions!(
 324    vim,
 325    [
 326        /// Moves cursor left one character.
 327        Left,
 328        /// Moves cursor left one character, wrapping to previous line.
 329        #[action(deprecated_aliases = ["vim::Backspace"])]
 330        WrappingLeft,
 331        /// Moves cursor right one character.
 332        Right,
 333        /// Moves cursor right one character, wrapping to next line.
 334        #[action(deprecated_aliases = ["vim::Space"])]
 335        WrappingRight,
 336        /// Selects the current line.
 337        CurrentLine,
 338        /// Moves to the start of the next sentence.
 339        SentenceForward,
 340        /// Moves to the start of the previous sentence.
 341        SentenceBackward,
 342        /// Moves to the start of the paragraph.
 343        StartOfParagraph,
 344        /// Moves to the end of the paragraph.
 345        EndOfParagraph,
 346        /// Moves to the start of the document.
 347        StartOfDocument,
 348        /// Moves to the end of the document.
 349        EndOfDocument,
 350        /// Moves to the matching bracket or delimiter.
 351        Matching,
 352        /// Goes to a percentage position in the file.
 353        GoToPercentage,
 354        /// Moves to the start of the next line.
 355        NextLineStart,
 356        /// Moves to the start of the previous line.
 357        PreviousLineStart,
 358        /// Moves to the start of a line downward.
 359        StartOfLineDownward,
 360        /// Moves to the end of a line downward.
 361        EndOfLineDownward,
 362        /// Goes to a specific column number.
 363        GoToColumn,
 364        /// Repeats the last character find.
 365        RepeatFind,
 366        /// Repeats the last character find in reverse.
 367        RepeatFindReversed,
 368        /// Moves to the top of the window.
 369        WindowTop,
 370        /// Moves to the middle of the window.
 371        WindowMiddle,
 372        /// Moves to the bottom of the window.
 373        WindowBottom,
 374        /// Moves to the start of the next section.
 375        NextSectionStart,
 376        /// Moves to the end of the next section.
 377        NextSectionEnd,
 378        /// Moves to the start of the previous section.
 379        PreviousSectionStart,
 380        /// Moves to the end of the previous section.
 381        PreviousSectionEnd,
 382        /// Moves to the start of the next method.
 383        NextMethodStart,
 384        /// Moves to the end of the next method.
 385        NextMethodEnd,
 386        /// Moves to the start of the previous method.
 387        PreviousMethodStart,
 388        /// Moves to the end of the previous method.
 389        PreviousMethodEnd,
 390        /// Moves to the next comment.
 391        NextComment,
 392        /// Moves to the previous comment.
 393        PreviousComment,
 394        /// Moves to the previous line with lesser indentation.
 395        PreviousLesserIndent,
 396        /// Moves to the previous line with greater indentation.
 397        PreviousGreaterIndent,
 398        /// Moves to the previous line with the same indentation.
 399        PreviousSameIndent,
 400        /// Moves to the next line with lesser indentation.
 401        NextLesserIndent,
 402        /// Moves to the next line with greater indentation.
 403        NextGreaterIndent,
 404        /// Moves to the next line with the same indentation.
 405        NextSameIndent,
 406    ]
 407);
 408
 409pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 410    Vim::action(editor, cx, |vim, _: &Left, window, cx| {
 411        vim.motion(Motion::Left, window, cx)
 412    });
 413    Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
 414        vim.motion(Motion::WrappingLeft, window, cx)
 415    });
 416    Vim::action(editor, cx, |vim, action: &Down, window, cx| {
 417        vim.motion(
 418            Motion::Down {
 419                display_lines: action.display_lines,
 420            },
 421            window,
 422            cx,
 423        )
 424    });
 425    Vim::action(editor, cx, |vim, action: &Up, window, cx| {
 426        vim.motion(
 427            Motion::Up {
 428                display_lines: action.display_lines,
 429            },
 430            window,
 431            cx,
 432        )
 433    });
 434    Vim::action(editor, cx, |vim, _: &Right, window, cx| {
 435        vim.motion(Motion::Right, window, cx)
 436    });
 437    Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
 438        vim.motion(Motion::WrappingRight, window, cx)
 439    });
 440    Vim::action(
 441        editor,
 442        cx,
 443        |vim, action: &FirstNonWhitespace, window, cx| {
 444            vim.motion(
 445                Motion::FirstNonWhitespace {
 446                    display_lines: action.display_lines,
 447                },
 448                window,
 449                cx,
 450            )
 451        },
 452    );
 453    Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
 454        vim.motion(
 455            Motion::StartOfLine {
 456                display_lines: action.display_lines,
 457            },
 458            window,
 459            cx,
 460        )
 461    });
 462    Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| {
 463        vim.motion(
 464            Motion::MiddleOfLine {
 465                display_lines: action.display_lines,
 466            },
 467            window,
 468            cx,
 469        )
 470    });
 471    Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
 472        vim.motion(
 473            Motion::EndOfLine {
 474                display_lines: action.display_lines,
 475            },
 476            window,
 477            cx,
 478        )
 479    });
 480    Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
 481        vim.motion(Motion::CurrentLine, window, cx)
 482    });
 483    Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
 484        vim.motion(Motion::StartOfParagraph, window, cx)
 485    });
 486    Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
 487        vim.motion(Motion::EndOfParagraph, window, cx)
 488    });
 489
 490    Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
 491        vim.motion(Motion::SentenceForward, window, cx)
 492    });
 493    Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
 494        vim.motion(Motion::SentenceBackward, window, cx)
 495    });
 496    Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
 497        vim.motion(Motion::StartOfDocument, window, cx)
 498    });
 499    Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
 500        vim.motion(Motion::EndOfDocument, window, cx)
 501    });
 502    Vim::action(editor, cx, |vim, _: &Matching, window, cx| {
 503        vim.motion(Motion::Matching, window, cx)
 504    });
 505    Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
 506        vim.motion(Motion::GoToPercentage, window, cx)
 507    });
 508    Vim::action(
 509        editor,
 510        cx,
 511        |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
 512            vim.motion(Motion::UnmatchedForward { char }, window, cx)
 513        },
 514    );
 515    Vim::action(
 516        editor,
 517        cx,
 518        |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
 519            vim.motion(Motion::UnmatchedBackward { char }, window, cx)
 520        },
 521    );
 522    Vim::action(
 523        editor,
 524        cx,
 525        |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
 526            vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
 527        },
 528    );
 529    Vim::action(
 530        editor,
 531        cx,
 532        |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
 533            vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
 534        },
 535    );
 536    Vim::action(
 537        editor,
 538        cx,
 539        |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
 540            vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
 541        },
 542    );
 543    Vim::action(
 544        editor,
 545        cx,
 546        |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
 547            vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
 548        },
 549    );
 550    Vim::action(
 551        editor,
 552        cx,
 553        |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
 554            vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
 555        },
 556    );
 557    Vim::action(
 558        editor,
 559        cx,
 560        |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
 561            vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
 562        },
 563    );
 564    Vim::action(
 565        editor,
 566        cx,
 567        |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
 568            vim.motion(
 569                Motion::PreviousSubwordStart { ignore_punctuation },
 570                window,
 571                cx,
 572            )
 573        },
 574    );
 575    Vim::action(
 576        editor,
 577        cx,
 578        |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
 579            vim.motion(
 580                Motion::PreviousSubwordEnd { ignore_punctuation },
 581                window,
 582                cx,
 583            )
 584        },
 585    );
 586    Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
 587        vim.motion(Motion::NextLineStart, window, cx)
 588    });
 589    Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
 590        vim.motion(Motion::PreviousLineStart, window, cx)
 591    });
 592    Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
 593        vim.motion(Motion::StartOfLineDownward, window, cx)
 594    });
 595    Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
 596        vim.motion(Motion::EndOfLineDownward, window, cx)
 597    });
 598    Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
 599        vim.motion(Motion::GoToColumn, window, cx)
 600    });
 601
 602    Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
 603        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 604            vim.motion(Motion::RepeatFind { last_find }, window, cx);
 605        }
 606    });
 607
 608    Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
 609        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 610            vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
 611        }
 612    });
 613    Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
 614        vim.motion(Motion::WindowTop, window, cx)
 615    });
 616    Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
 617        vim.motion(Motion::WindowMiddle, window, cx)
 618    });
 619    Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
 620        vim.motion(Motion::WindowBottom, window, cx)
 621    });
 622
 623    Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
 624        vim.motion(Motion::PreviousSectionStart, window, cx)
 625    });
 626    Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
 627        vim.motion(Motion::NextSectionStart, window, cx)
 628    });
 629    Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
 630        vim.motion(Motion::PreviousSectionEnd, window, cx)
 631    });
 632    Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
 633        vim.motion(Motion::NextSectionEnd, window, cx)
 634    });
 635    Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
 636        vim.motion(Motion::PreviousMethodStart, window, cx)
 637    });
 638    Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
 639        vim.motion(Motion::NextMethodStart, window, cx)
 640    });
 641    Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
 642        vim.motion(Motion::PreviousMethodEnd, window, cx)
 643    });
 644    Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
 645        vim.motion(Motion::NextMethodEnd, window, cx)
 646    });
 647    Vim::action(editor, cx, |vim, &NextComment, window, cx| {
 648        vim.motion(Motion::NextComment, window, cx)
 649    });
 650    Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
 651        vim.motion(Motion::PreviousComment, window, cx)
 652    });
 653    Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| {
 654        vim.motion(Motion::PreviousLesserIndent, window, cx)
 655    });
 656    Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| {
 657        vim.motion(Motion::PreviousGreaterIndent, window, cx)
 658    });
 659    Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| {
 660        vim.motion(Motion::PreviousSameIndent, window, cx)
 661    });
 662    Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| {
 663        vim.motion(Motion::NextLesserIndent, window, cx)
 664    });
 665    Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| {
 666        vim.motion(Motion::NextGreaterIndent, window, cx)
 667    });
 668    Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| {
 669        vim.motion(Motion::NextSameIndent, window, cx)
 670    });
 671}
 672
 673impl Vim {
 674    pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
 675        if let Motion::ZedSearchResult {
 676            prior_selections, ..
 677        } = &m
 678        {
 679            match self.mode {
 680                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 681                    if !prior_selections.is_empty() {
 682                        self.update_editor(cx, |_, editor, cx| {
 683                            editor.change_selections(Default::default(), window, cx, |s| {
 684                                s.select_ranges(prior_selections.iter().cloned())
 685                            })
 686                        });
 687                    }
 688                }
 689                Mode::Normal | Mode::Replace | Mode::Insert => {
 690                    if self.active_operator().is_none() {
 691                        return;
 692                    }
 693                }
 694
 695                Mode::HelixNormal => {}
 696            }
 697        }
 698
 699        self.motion(m, window, cx)
 700    }
 701
 702    pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context<Self>) {
 703        if let Some(Operator::FindForward { .. })
 704        | Some(Operator::Sneak { .. })
 705        | Some(Operator::SneakBackward { .. })
 706        | Some(Operator::FindBackward { .. }) = self.active_operator()
 707        {
 708            self.pop_operator(window, cx);
 709        }
 710
 711        let count = Vim::take_count(cx);
 712        let forced_motion = Vim::take_forced_motion(cx);
 713        let active_operator = self.active_operator();
 714        let mut waiting_operator: Option<Operator> = None;
 715        match self.mode {
 716            Mode::Normal | Mode::Replace | Mode::Insert => {
 717                if active_operator == Some(Operator::AddSurrounds { target: None }) {
 718                    waiting_operator = Some(Operator::AddSurrounds {
 719                        target: Some(SurroundsType::Motion(motion)),
 720                    });
 721                } else {
 722                    self.normal_motion(motion, active_operator, count, forced_motion, window, cx)
 723                }
 724            }
 725            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 726                self.visual_motion(motion, count, window, cx)
 727            }
 728
 729            Mode::HelixNormal => self.helix_normal_motion(motion, count, window, cx),
 730        }
 731        self.clear_operator(window, cx);
 732        if let Some(operator) = waiting_operator {
 733            self.push_operator(operator, window, cx);
 734            Vim::globals(cx).pre_count = count
 735        }
 736    }
 737}
 738
 739// Motion handling is specified here:
 740// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 741impl Motion {
 742    fn default_kind(&self) -> MotionKind {
 743        use Motion::*;
 744        match self {
 745            Down { .. }
 746            | Up { .. }
 747            | StartOfDocument
 748            | EndOfDocument
 749            | CurrentLine
 750            | NextLineStart
 751            | PreviousLineStart
 752            | StartOfLineDownward
 753            | WindowTop
 754            | WindowMiddle
 755            | WindowBottom
 756            | NextSectionStart
 757            | NextSectionEnd
 758            | PreviousSectionStart
 759            | PreviousSectionEnd
 760            | NextMethodStart
 761            | NextMethodEnd
 762            | PreviousMethodStart
 763            | PreviousMethodEnd
 764            | NextComment
 765            | PreviousComment
 766            | PreviousLesserIndent
 767            | PreviousGreaterIndent
 768            | PreviousSameIndent
 769            | NextLesserIndent
 770            | NextGreaterIndent
 771            | NextSameIndent
 772            | GoToPercentage
 773            | Jump { line: true, .. } => MotionKind::Linewise,
 774            EndOfLine { .. }
 775            | EndOfLineDownward
 776            | Matching
 777            | FindForward { .. }
 778            | NextWordEnd { .. }
 779            | PreviousWordEnd { .. }
 780            | NextSubwordEnd { .. }
 781            | PreviousSubwordEnd { .. } => MotionKind::Inclusive,
 782            Left
 783            | WrappingLeft
 784            | Right
 785            | WrappingRight
 786            | StartOfLine { .. }
 787            | StartOfParagraph
 788            | EndOfParagraph
 789            | SentenceBackward
 790            | SentenceForward
 791            | GoToColumn
 792            | MiddleOfLine { .. }
 793            | UnmatchedForward { .. }
 794            | UnmatchedBackward { .. }
 795            | NextWordStart { .. }
 796            | PreviousWordStart { .. }
 797            | NextSubwordStart { .. }
 798            | PreviousSubwordStart { .. }
 799            | FirstNonWhitespace { .. }
 800            | FindBackward { .. }
 801            | Sneak { .. }
 802            | SneakBackward { .. }
 803            | Jump { .. }
 804            | ZedSearchResult { .. } => MotionKind::Exclusive,
 805            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 806                motion.default_kind()
 807            }
 808        }
 809    }
 810
 811    fn skip_exclusive_special_case(&self) -> bool {
 812        matches!(self, Motion::WrappingLeft | Motion::WrappingRight)
 813    }
 814
 815    pub(crate) fn push_to_jump_list(&self) -> bool {
 816        use Motion::*;
 817        match self {
 818            CurrentLine
 819            | Down { .. }
 820            | EndOfLine { .. }
 821            | EndOfLineDownward
 822            | FindBackward { .. }
 823            | FindForward { .. }
 824            | FirstNonWhitespace { .. }
 825            | GoToColumn
 826            | Left
 827            | MiddleOfLine { .. }
 828            | NextLineStart
 829            | NextSubwordEnd { .. }
 830            | NextSubwordStart { .. }
 831            | NextWordEnd { .. }
 832            | NextWordStart { .. }
 833            | PreviousLineStart
 834            | PreviousSubwordEnd { .. }
 835            | PreviousSubwordStart { .. }
 836            | PreviousWordEnd { .. }
 837            | PreviousWordStart { .. }
 838            | RepeatFind { .. }
 839            | RepeatFindReversed { .. }
 840            | Right
 841            | StartOfLine { .. }
 842            | StartOfLineDownward
 843            | Up { .. }
 844            | WrappingLeft
 845            | WrappingRight => false,
 846            EndOfDocument
 847            | EndOfParagraph
 848            | GoToPercentage
 849            | Jump { .. }
 850            | Matching
 851            | NextComment
 852            | NextGreaterIndent
 853            | NextLesserIndent
 854            | NextMethodEnd
 855            | NextMethodStart
 856            | NextSameIndent
 857            | NextSectionEnd
 858            | NextSectionStart
 859            | PreviousComment
 860            | PreviousGreaterIndent
 861            | PreviousLesserIndent
 862            | PreviousMethodEnd
 863            | PreviousMethodStart
 864            | PreviousSameIndent
 865            | PreviousSectionEnd
 866            | PreviousSectionStart
 867            | SentenceBackward
 868            | SentenceForward
 869            | Sneak { .. }
 870            | SneakBackward { .. }
 871            | StartOfDocument
 872            | StartOfParagraph
 873            | UnmatchedBackward { .. }
 874            | UnmatchedForward { .. }
 875            | WindowBottom
 876            | WindowMiddle
 877            | WindowTop
 878            | ZedSearchResult { .. } => true,
 879        }
 880    }
 881
 882    pub fn infallible(&self) -> bool {
 883        use Motion::*;
 884        match self {
 885            StartOfDocument | EndOfDocument | CurrentLine => true,
 886            Down { .. }
 887            | Up { .. }
 888            | EndOfLine { .. }
 889            | MiddleOfLine { .. }
 890            | Matching
 891            | UnmatchedForward { .. }
 892            | UnmatchedBackward { .. }
 893            | FindForward { .. }
 894            | RepeatFind { .. }
 895            | Left
 896            | WrappingLeft
 897            | Right
 898            | WrappingRight
 899            | StartOfLine { .. }
 900            | StartOfParagraph
 901            | EndOfParagraph
 902            | SentenceBackward
 903            | SentenceForward
 904            | StartOfLineDownward
 905            | EndOfLineDownward
 906            | GoToColumn
 907            | GoToPercentage
 908            | NextWordStart { .. }
 909            | NextWordEnd { .. }
 910            | PreviousWordStart { .. }
 911            | PreviousWordEnd { .. }
 912            | NextSubwordStart { .. }
 913            | NextSubwordEnd { .. }
 914            | PreviousSubwordStart { .. }
 915            | PreviousSubwordEnd { .. }
 916            | FirstNonWhitespace { .. }
 917            | FindBackward { .. }
 918            | Sneak { .. }
 919            | SneakBackward { .. }
 920            | RepeatFindReversed { .. }
 921            | WindowTop
 922            | WindowMiddle
 923            | WindowBottom
 924            | NextLineStart
 925            | PreviousLineStart
 926            | ZedSearchResult { .. }
 927            | NextSectionStart
 928            | NextSectionEnd
 929            | PreviousSectionStart
 930            | PreviousSectionEnd
 931            | NextMethodStart
 932            | NextMethodEnd
 933            | PreviousMethodStart
 934            | PreviousMethodEnd
 935            | NextComment
 936            | PreviousComment
 937            | PreviousLesserIndent
 938            | PreviousGreaterIndent
 939            | PreviousSameIndent
 940            | NextLesserIndent
 941            | NextGreaterIndent
 942            | NextSameIndent
 943            | Jump { .. } => false,
 944        }
 945    }
 946
 947    pub fn move_point(
 948        &self,
 949        map: &DisplaySnapshot,
 950        point: DisplayPoint,
 951        goal: SelectionGoal,
 952        maybe_times: Option<usize>,
 953        text_layout_details: &TextLayoutDetails,
 954    ) -> Option<(DisplayPoint, SelectionGoal)> {
 955        let times = maybe_times.unwrap_or(1);
 956        use Motion::*;
 957        let infallible = self.infallible();
 958        let (new_point, goal) = match self {
 959            Left => (left(map, point, times), SelectionGoal::None),
 960            WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None),
 961            Down {
 962                display_lines: false,
 963            } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
 964            Down {
 965                display_lines: true,
 966            } => down_display(map, point, goal, times, text_layout_details),
 967            Up {
 968                display_lines: false,
 969            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
 970            Up {
 971                display_lines: true,
 972            } => up_display(map, point, goal, times, text_layout_details),
 973            Right => (right(map, point, times), SelectionGoal::None),
 974            WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None),
 975            NextWordStart { ignore_punctuation } => (
 976                next_word_start(map, point, *ignore_punctuation, times),
 977                SelectionGoal::None,
 978            ),
 979            NextWordEnd { ignore_punctuation } => (
 980                next_word_end(map, point, *ignore_punctuation, times, true, true),
 981                SelectionGoal::None,
 982            ),
 983            PreviousWordStart { ignore_punctuation } => (
 984                previous_word_start(map, point, *ignore_punctuation, times),
 985                SelectionGoal::None,
 986            ),
 987            PreviousWordEnd { ignore_punctuation } => (
 988                previous_word_end(map, point, *ignore_punctuation, times),
 989                SelectionGoal::None,
 990            ),
 991            NextSubwordStart { ignore_punctuation } => (
 992                next_subword_start(map, point, *ignore_punctuation, times),
 993                SelectionGoal::None,
 994            ),
 995            NextSubwordEnd { ignore_punctuation } => (
 996                next_subword_end(map, point, *ignore_punctuation, times, true),
 997                SelectionGoal::None,
 998            ),
 999            PreviousSubwordStart { ignore_punctuation } => (
1000                previous_subword_start(map, point, *ignore_punctuation, times),
1001                SelectionGoal::None,
1002            ),
1003            PreviousSubwordEnd { ignore_punctuation } => (
1004                previous_subword_end(map, point, *ignore_punctuation, times),
1005                SelectionGoal::None,
1006            ),
1007            FirstNonWhitespace { display_lines } => (
1008                first_non_whitespace(map, *display_lines, point),
1009                SelectionGoal::None,
1010            ),
1011            StartOfLine { display_lines } => (
1012                start_of_line(map, *display_lines, point),
1013                SelectionGoal::None,
1014            ),
1015            MiddleOfLine { display_lines } => (
1016                middle_of_line(map, *display_lines, point, maybe_times),
1017                SelectionGoal::None,
1018            ),
1019            EndOfLine { display_lines } => (
1020                end_of_line(map, *display_lines, point, times),
1021                SelectionGoal::None,
1022            ),
1023            SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
1024            SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
1025            StartOfParagraph => (
1026                movement::start_of_paragraph(map, point, times),
1027                SelectionGoal::None,
1028            ),
1029            EndOfParagraph => (
1030                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
1031                SelectionGoal::None,
1032            ),
1033            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
1034            StartOfDocument => (
1035                start_of_document(map, point, maybe_times),
1036                SelectionGoal::None,
1037            ),
1038            EndOfDocument => (
1039                end_of_document(map, point, maybe_times),
1040                SelectionGoal::None,
1041            ),
1042            Matching => (matching(map, point), SelectionGoal::None),
1043            GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None),
1044            UnmatchedForward { char } => (
1045                unmatched_forward(map, point, *char, times),
1046                SelectionGoal::None,
1047            ),
1048            UnmatchedBackward { char } => (
1049                unmatched_backward(map, point, *char, times),
1050                SelectionGoal::None,
1051            ),
1052            // t f
1053            FindForward {
1054                before,
1055                char,
1056                mode,
1057                smartcase,
1058            } => {
1059                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
1060                    .map(|new_point| (new_point, SelectionGoal::None));
1061            }
1062            // T F
1063            FindBackward {
1064                after,
1065                char,
1066                mode,
1067                smartcase,
1068            } => (
1069                find_backward(map, point, *after, *char, times, *mode, *smartcase),
1070                SelectionGoal::None,
1071            ),
1072            Sneak {
1073                first_char,
1074                second_char,
1075                smartcase,
1076            } => {
1077                return sneak(map, point, *first_char, *second_char, times, *smartcase)
1078                    .map(|new_point| (new_point, SelectionGoal::None));
1079            }
1080            SneakBackward {
1081                first_char,
1082                second_char,
1083                smartcase,
1084            } => {
1085                return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
1086                    .map(|new_point| (new_point, SelectionGoal::None));
1087            }
1088            // ; -- repeat the last find done with t, f, T, F
1089            RepeatFind { last_find } => match **last_find {
1090                Motion::FindForward {
1091                    before,
1092                    char,
1093                    mode,
1094                    smartcase,
1095                } => {
1096                    let mut new_point =
1097                        find_forward(map, point, before, char, times, mode, smartcase);
1098                    if new_point == Some(point) {
1099                        new_point =
1100                            find_forward(map, point, before, char, times + 1, mode, smartcase);
1101                    }
1102
1103                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1104                }
1105
1106                Motion::FindBackward {
1107                    after,
1108                    char,
1109                    mode,
1110                    smartcase,
1111                } => {
1112                    let mut new_point =
1113                        find_backward(map, point, after, char, times, mode, smartcase);
1114                    if new_point == point {
1115                        new_point =
1116                            find_backward(map, point, after, char, times + 1, mode, smartcase);
1117                    }
1118
1119                    (new_point, SelectionGoal::None)
1120                }
1121                Motion::Sneak {
1122                    first_char,
1123                    second_char,
1124                    smartcase,
1125                } => {
1126                    let mut new_point =
1127                        sneak(map, point, first_char, second_char, times, smartcase);
1128                    if new_point == Some(point) {
1129                        new_point =
1130                            sneak(map, point, first_char, second_char, times + 1, smartcase);
1131                    }
1132
1133                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1134                }
1135
1136                Motion::SneakBackward {
1137                    first_char,
1138                    second_char,
1139                    smartcase,
1140                } => {
1141                    let mut new_point =
1142                        sneak_backward(map, point, first_char, second_char, times, smartcase);
1143                    if new_point == Some(point) {
1144                        new_point = sneak_backward(
1145                            map,
1146                            point,
1147                            first_char,
1148                            second_char,
1149                            times + 1,
1150                            smartcase,
1151                        );
1152                    }
1153
1154                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1155                }
1156                _ => return None,
1157            },
1158            // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
1159            RepeatFindReversed { last_find } => match **last_find {
1160                Motion::FindForward {
1161                    before,
1162                    char,
1163                    mode,
1164                    smartcase,
1165                } => {
1166                    let mut new_point =
1167                        find_backward(map, point, before, char, times, mode, smartcase);
1168                    if new_point == point {
1169                        new_point =
1170                            find_backward(map, point, before, char, times + 1, mode, smartcase);
1171                    }
1172
1173                    (new_point, SelectionGoal::None)
1174                }
1175
1176                Motion::FindBackward {
1177                    after,
1178                    char,
1179                    mode,
1180                    smartcase,
1181                } => {
1182                    let mut new_point =
1183                        find_forward(map, point, after, char, times, mode, smartcase);
1184                    if new_point == Some(point) {
1185                        new_point =
1186                            find_forward(map, point, after, char, times + 1, mode, smartcase);
1187                    }
1188
1189                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1190                }
1191
1192                Motion::Sneak {
1193                    first_char,
1194                    second_char,
1195                    smartcase,
1196                } => {
1197                    let mut new_point =
1198                        sneak_backward(map, point, first_char, second_char, times, smartcase);
1199                    if new_point == Some(point) {
1200                        new_point = sneak_backward(
1201                            map,
1202                            point,
1203                            first_char,
1204                            second_char,
1205                            times + 1,
1206                            smartcase,
1207                        );
1208                    }
1209
1210                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1211                }
1212
1213                Motion::SneakBackward {
1214                    first_char,
1215                    second_char,
1216                    smartcase,
1217                } => {
1218                    let mut new_point =
1219                        sneak(map, point, first_char, second_char, times, smartcase);
1220                    if new_point == Some(point) {
1221                        new_point =
1222                            sneak(map, point, first_char, second_char, times + 1, smartcase);
1223                    }
1224
1225                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1226                }
1227                _ => return None,
1228            },
1229            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
1230            PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
1231            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
1232            EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
1233            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
1234            WindowTop => window_top(map, point, text_layout_details, times - 1),
1235            WindowMiddle => window_middle(map, point, text_layout_details),
1236            WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
1237            Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
1238            ZedSearchResult { new_selections, .. } => {
1239                // There will be only one selection, as
1240                // Search::SelectNextMatch selects a single match.
1241                if let Some(new_selection) = new_selections.first() {
1242                    (
1243                        new_selection.start.to_display_point(map),
1244                        SelectionGoal::None,
1245                    )
1246                } else {
1247                    return None;
1248                }
1249            }
1250            NextSectionStart => (
1251                section_motion(map, point, times, Direction::Next, true),
1252                SelectionGoal::None,
1253            ),
1254            NextSectionEnd => (
1255                section_motion(map, point, times, Direction::Next, false),
1256                SelectionGoal::None,
1257            ),
1258            PreviousSectionStart => (
1259                section_motion(map, point, times, Direction::Prev, true),
1260                SelectionGoal::None,
1261            ),
1262            PreviousSectionEnd => (
1263                section_motion(map, point, times, Direction::Prev, false),
1264                SelectionGoal::None,
1265            ),
1266
1267            NextMethodStart => (
1268                method_motion(map, point, times, Direction::Next, true),
1269                SelectionGoal::None,
1270            ),
1271            NextMethodEnd => (
1272                method_motion(map, point, times, Direction::Next, false),
1273                SelectionGoal::None,
1274            ),
1275            PreviousMethodStart => (
1276                method_motion(map, point, times, Direction::Prev, true),
1277                SelectionGoal::None,
1278            ),
1279            PreviousMethodEnd => (
1280                method_motion(map, point, times, Direction::Prev, false),
1281                SelectionGoal::None,
1282            ),
1283            NextComment => (
1284                comment_motion(map, point, times, Direction::Next),
1285                SelectionGoal::None,
1286            ),
1287            PreviousComment => (
1288                comment_motion(map, point, times, Direction::Prev),
1289                SelectionGoal::None,
1290            ),
1291            PreviousLesserIndent => (
1292                indent_motion(map, point, times, Direction::Prev, IndentType::Lesser),
1293                SelectionGoal::None,
1294            ),
1295            PreviousGreaterIndent => (
1296                indent_motion(map, point, times, Direction::Prev, IndentType::Greater),
1297                SelectionGoal::None,
1298            ),
1299            PreviousSameIndent => (
1300                indent_motion(map, point, times, Direction::Prev, IndentType::Same),
1301                SelectionGoal::None,
1302            ),
1303            NextLesserIndent => (
1304                indent_motion(map, point, times, Direction::Next, IndentType::Lesser),
1305                SelectionGoal::None,
1306            ),
1307            NextGreaterIndent => (
1308                indent_motion(map, point, times, Direction::Next, IndentType::Greater),
1309                SelectionGoal::None,
1310            ),
1311            NextSameIndent => (
1312                indent_motion(map, point, times, Direction::Next, IndentType::Same),
1313                SelectionGoal::None,
1314            ),
1315        };
1316        (new_point != point || infallible).then_some((new_point, goal))
1317    }
1318
1319    // Get the range value after self is applied to the specified selection.
1320    pub fn range(
1321        &self,
1322        map: &DisplaySnapshot,
1323        mut selection: Selection<DisplayPoint>,
1324        times: Option<usize>,
1325        text_layout_details: &TextLayoutDetails,
1326        forced_motion: bool,
1327    ) -> Option<(Range<DisplayPoint>, MotionKind)> {
1328        if let Motion::ZedSearchResult {
1329            prior_selections,
1330            new_selections,
1331        } = self
1332        {
1333            if let Some((prior_selection, new_selection)) =
1334                prior_selections.first().zip(new_selections.first())
1335            {
1336                let start = prior_selection
1337                    .start
1338                    .to_display_point(map)
1339                    .min(new_selection.start.to_display_point(map));
1340                let end = new_selection
1341                    .end
1342                    .to_display_point(map)
1343                    .max(prior_selection.end.to_display_point(map));
1344
1345                if start < end {
1346                    return Some((start..end, MotionKind::Exclusive));
1347                } else {
1348                    return Some((end..start, MotionKind::Exclusive));
1349                }
1350            } else {
1351                return None;
1352            }
1353        }
1354        let maybe_new_point = self.move_point(
1355            map,
1356            selection.head(),
1357            selection.goal,
1358            times,
1359            text_layout_details,
1360        );
1361
1362        let (new_head, goal) = match (maybe_new_point, forced_motion) {
1363            (Some((p, g)), _) => Some((p, g)),
1364            (None, false) => None,
1365            (None, true) => Some((selection.head(), selection.goal)),
1366        }?;
1367
1368        selection.set_head(new_head, goal);
1369
1370        let mut kind = match (self.default_kind(), forced_motion) {
1371            (MotionKind::Linewise, true) => MotionKind::Exclusive,
1372            (MotionKind::Exclusive, true) => MotionKind::Inclusive,
1373            (MotionKind::Inclusive, true) => MotionKind::Exclusive,
1374            (kind, false) => kind,
1375        };
1376
1377        if let Motion::NextWordStart {
1378            ignore_punctuation: _,
1379        } = self
1380        {
1381            // Another special case: When using the "w" motion in combination with an
1382            // operator and the last word moved over is at the end of a line, the end of
1383            // that word becomes the end of the operated text, not the first word in the
1384            // next line.
1385            let start = selection.start.to_point(map);
1386            let end = selection.end.to_point(map);
1387            let start_row = MultiBufferRow(selection.start.to_point(map).row);
1388            if end.row > start.row {
1389                selection.end = Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
1390                    .to_display_point(map);
1391
1392                // a bit of a hack, we need `cw` on a blank line to not delete the newline,
1393                // but dw on a blank line should. The `Linewise` returned from this method
1394                // causes the `d` operator to include the trailing newline.
1395                if selection.start == selection.end {
1396                    return Some((selection.start..selection.end, MotionKind::Linewise));
1397                }
1398            }
1399        } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() {
1400            let start_point = selection.start.to_point(map);
1401            let mut end_point = selection.end.to_point(map);
1402            let mut next_point = selection.end;
1403            *next_point.column_mut() += 1;
1404            next_point = map.clip_point(next_point, Bias::Right);
1405            if next_point.to_point(map) == end_point && forced_motion {
1406                selection.end = movement::saturating_left(map, selection.end);
1407            }
1408
1409            if end_point.row > start_point.row {
1410                let first_non_blank_of_start_row = map
1411                    .line_indent_for_buffer_row(MultiBufferRow(start_point.row))
1412                    .raw_len();
1413                // https://github.com/neovim/neovim/blob/ee143aaf65a0e662c42c636aa4a959682858b3e7/src/nvim/ops.c#L6178-L6203
1414                if end_point.column == 0 {
1415                    // If the motion is exclusive and the end of the motion is in column 1, the
1416                    // end of the motion is moved to the end of the previous line and the motion
1417                    // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1418                    // but "d}" will not include that line.
1419                    //
1420                    // If the motion is exclusive, the end of the motion is in column 1 and the
1421                    // start of the motion was at or before the first non-blank in the line, the
1422                    // motion becomes linewise.  Example: If a paragraph begins with some blanks
1423                    // and you do "d}" while standing on the first non-blank, all the lines of
1424                    // the paragraph are deleted, including the blanks.
1425                    if start_point.column <= first_non_blank_of_start_row {
1426                        kind = MotionKind::Linewise;
1427                    } else {
1428                        kind = MotionKind::Inclusive;
1429                    }
1430                    end_point.row -= 1;
1431                    end_point.column = 0;
1432                    selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1433                } else if let Motion::EndOfParagraph = self {
1434                    // Special case: When using the "}" motion, it's possible
1435                    // that there's no blank lines after the paragraph the
1436                    // cursor is currently on.
1437                    // In this situation the `end_point.column` value will be
1438                    // greater than 0, so the selection doesn't actually end on
1439                    // the first character of a blank line. In that case, we'll
1440                    // want to move one column to the right, to actually include
1441                    // all characters of the last non-blank line.
1442                    selection.end = movement::saturating_right(map, selection.end)
1443                }
1444            }
1445        } else if kind == MotionKind::Inclusive {
1446            selection.end = movement::saturating_right(map, selection.end)
1447        }
1448
1449        if kind == MotionKind::Linewise {
1450            selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1451            selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1452        }
1453        Some((selection.start..selection.end, kind))
1454    }
1455
1456    // Expands a selection using self for an operator
1457    pub fn expand_selection(
1458        &self,
1459        map: &DisplaySnapshot,
1460        selection: &mut Selection<DisplayPoint>,
1461        times: Option<usize>,
1462        text_layout_details: &TextLayoutDetails,
1463        forced_motion: bool,
1464    ) -> Option<MotionKind> {
1465        let (range, kind) = self.range(
1466            map,
1467            selection.clone(),
1468            times,
1469            text_layout_details,
1470            forced_motion,
1471        )?;
1472        selection.start = range.start;
1473        selection.end = range.end;
1474        Some(kind)
1475    }
1476}
1477
1478fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1479    for _ in 0..times {
1480        point = movement::saturating_left(map, point);
1481        if point.column() == 0 {
1482            break;
1483        }
1484    }
1485    point
1486}
1487
1488pub(crate) fn wrapping_left(
1489    map: &DisplaySnapshot,
1490    mut point: DisplayPoint,
1491    times: usize,
1492) -> DisplayPoint {
1493    for _ in 0..times {
1494        point = movement::left(map, point);
1495        if point.is_zero() {
1496            break;
1497        }
1498    }
1499    point
1500}
1501
1502fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1503    for _ in 0..times {
1504        point = wrapping_right_single(map, point);
1505        if point == map.max_point() {
1506            break;
1507        }
1508    }
1509    point
1510}
1511
1512fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
1513    let mut next_point = point;
1514    *next_point.column_mut() += 1;
1515    next_point = map.clip_point(next_point, Bias::Right);
1516    if next_point == point {
1517        if next_point.row() == map.max_point().row() {
1518            next_point
1519        } else {
1520            DisplayPoint::new(next_point.row().next_row(), 0)
1521        }
1522    } else {
1523        next_point
1524    }
1525}
1526
1527pub(crate) fn start_of_relative_buffer_row(
1528    map: &DisplaySnapshot,
1529    point: DisplayPoint,
1530    times: isize,
1531) -> DisplayPoint {
1532    let start = map.display_point_to_fold_point(point, Bias::Left);
1533    let target = start.row() as isize + times;
1534    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1535
1536    map.clip_point(
1537        map.fold_point_to_display_point(
1538            map.fold_snapshot
1539                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1540        ),
1541        Bias::Right,
1542    )
1543}
1544
1545fn up_down_buffer_rows(
1546    map: &DisplaySnapshot,
1547    mut point: DisplayPoint,
1548    mut goal: SelectionGoal,
1549    mut times: isize,
1550    text_layout_details: &TextLayoutDetails,
1551) -> (DisplayPoint, SelectionGoal) {
1552    let bias = if times < 0 { Bias::Left } else { Bias::Right };
1553
1554    while map.is_folded_buffer_header(point.row()) {
1555        if times < 0 {
1556            (point, _) = movement::up(map, point, goal, true, text_layout_details);
1557            times += 1;
1558        } else if times > 0 {
1559            (point, _) = movement::down(map, point, goal, true, text_layout_details);
1560            times -= 1;
1561        } else {
1562            break;
1563        }
1564    }
1565
1566    let start = map.display_point_to_fold_point(point, Bias::Left);
1567    let begin_folded_line = map.fold_point_to_display_point(
1568        map.fold_snapshot
1569            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1570    );
1571    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1572
1573    let (goal_wrap, goal_x) = match goal {
1574        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1575        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1576        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1577        _ => {
1578            let x = map.x_for_display_point(point, text_layout_details);
1579            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1580            (select_nth_wrapped_row, x.0)
1581        }
1582    };
1583
1584    let target = start.row() as isize + times;
1585    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1586
1587    let mut begin_folded_line = map.fold_point_to_display_point(
1588        map.fold_snapshot
1589            .clip_point(FoldPoint::new(new_row, 0), bias),
1590    );
1591
1592    let mut i = 0;
1593    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1594        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1595        if map
1596            .display_point_to_fold_point(next_folded_line, bias)
1597            .row()
1598            == new_row
1599        {
1600            i += 1;
1601            begin_folded_line = next_folded_line;
1602        } else {
1603            break;
1604        }
1605    }
1606
1607    let new_col = if i == goal_wrap {
1608        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1609    } else {
1610        map.line_len(begin_folded_line.row())
1611    };
1612
1613    let point = DisplayPoint::new(begin_folded_line.row(), new_col);
1614    let mut clipped_point = map.clip_point(point, bias);
1615
1616    // When navigating vertically in vim mode with inlay hints present,
1617    // we need to handle the case where clipping moves us to a different row.
1618    // This can happen when moving down (Bias::Right) and hitting an inlay hint.
1619    // Re-clip with opposite bias to stay on the intended line.
1620    //
1621    // See: https://github.com/zed-industries/zed/issues/29134
1622    if clipped_point.row() > point.row() {
1623        clipped_point = map.clip_point(point, Bias::Left);
1624    }
1625
1626    (clipped_point, goal)
1627}
1628
1629fn down_display(
1630    map: &DisplaySnapshot,
1631    mut point: DisplayPoint,
1632    mut goal: SelectionGoal,
1633    times: usize,
1634    text_layout_details: &TextLayoutDetails,
1635) -> (DisplayPoint, SelectionGoal) {
1636    for _ in 0..times {
1637        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1638    }
1639
1640    (point, goal)
1641}
1642
1643fn up_display(
1644    map: &DisplaySnapshot,
1645    mut point: DisplayPoint,
1646    mut goal: SelectionGoal,
1647    times: usize,
1648    text_layout_details: &TextLayoutDetails,
1649) -> (DisplayPoint, SelectionGoal) {
1650    for _ in 0..times {
1651        (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1652    }
1653
1654    (point, goal)
1655}
1656
1657pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1658    for _ in 0..times {
1659        let new_point = movement::saturating_right(map, point);
1660        if point == new_point {
1661            break;
1662        }
1663        point = new_point;
1664    }
1665    point
1666}
1667
1668pub(crate) fn next_char(
1669    map: &DisplaySnapshot,
1670    point: DisplayPoint,
1671    allow_cross_newline: bool,
1672) -> DisplayPoint {
1673    let mut new_point = point;
1674    let mut max_column = map.line_len(new_point.row());
1675    if !allow_cross_newline {
1676        max_column -= 1;
1677    }
1678    if new_point.column() < max_column {
1679        *new_point.column_mut() += 1;
1680    } else if new_point < map.max_point() && allow_cross_newline {
1681        *new_point.row_mut() += 1;
1682        *new_point.column_mut() = 0;
1683    }
1684    map.clip_ignoring_line_ends(new_point, Bias::Right)
1685}
1686
1687pub(crate) fn next_word_start(
1688    map: &DisplaySnapshot,
1689    mut point: DisplayPoint,
1690    ignore_punctuation: bool,
1691    times: usize,
1692) -> DisplayPoint {
1693    let classifier = map
1694        .buffer_snapshot
1695        .char_classifier_at(point.to_point(map))
1696        .ignore_punctuation(ignore_punctuation);
1697    for _ in 0..times {
1698        let mut crossed_newline = false;
1699        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1700            let left_kind = classifier.kind(left);
1701            let right_kind = classifier.kind(right);
1702            let at_newline = right == '\n';
1703
1704            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1705                || at_newline && crossed_newline
1706                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1707
1708            crossed_newline |= at_newline;
1709            found
1710        });
1711        if point == new_point {
1712            break;
1713        }
1714        point = new_point;
1715    }
1716    point
1717}
1718
1719pub(crate) fn next_word_end(
1720    map: &DisplaySnapshot,
1721    mut point: DisplayPoint,
1722    ignore_punctuation: bool,
1723    times: usize,
1724    allow_cross_newline: bool,
1725    always_advance: bool,
1726) -> DisplayPoint {
1727    let classifier = map
1728        .buffer_snapshot
1729        .char_classifier_at(point.to_point(map))
1730        .ignore_punctuation(ignore_punctuation);
1731    for _ in 0..times {
1732        let mut need_next_char = false;
1733        let new_point = if always_advance {
1734            next_char(map, point, allow_cross_newline)
1735        } else {
1736            point
1737        };
1738        let new_point = movement::find_boundary_exclusive(
1739            map,
1740            new_point,
1741            FindRange::MultiLine,
1742            |left, right| {
1743                let left_kind = classifier.kind(left);
1744                let right_kind = classifier.kind(right);
1745                let at_newline = right == '\n';
1746
1747                if !allow_cross_newline && at_newline {
1748                    need_next_char = true;
1749                    return true;
1750                }
1751
1752                left_kind != right_kind && left_kind != CharKind::Whitespace
1753            },
1754        );
1755        let new_point = if need_next_char {
1756            next_char(map, new_point, true)
1757        } else {
1758            new_point
1759        };
1760        let new_point = map.clip_point(new_point, Bias::Left);
1761        if point == new_point {
1762            break;
1763        }
1764        point = new_point;
1765    }
1766    point
1767}
1768
1769fn previous_word_start(
1770    map: &DisplaySnapshot,
1771    mut point: DisplayPoint,
1772    ignore_punctuation: bool,
1773    times: usize,
1774) -> DisplayPoint {
1775    let classifier = map
1776        .buffer_snapshot
1777        .char_classifier_at(point.to_point(map))
1778        .ignore_punctuation(ignore_punctuation);
1779    for _ in 0..times {
1780        // This works even though find_preceding_boundary is called for every character in the line containing
1781        // cursor because the newline is checked only once.
1782        let new_point = movement::find_preceding_boundary_display_point(
1783            map,
1784            point,
1785            FindRange::MultiLine,
1786            |left, right| {
1787                let left_kind = classifier.kind(left);
1788                let right_kind = classifier.kind(right);
1789
1790                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1791            },
1792        );
1793        if point == new_point {
1794            break;
1795        }
1796        point = new_point;
1797    }
1798    point
1799}
1800
1801fn previous_word_end(
1802    map: &DisplaySnapshot,
1803    point: DisplayPoint,
1804    ignore_punctuation: bool,
1805    times: usize,
1806) -> DisplayPoint {
1807    let classifier = map
1808        .buffer_snapshot
1809        .char_classifier_at(point.to_point(map))
1810        .ignore_punctuation(ignore_punctuation);
1811    let mut point = point.to_point(map);
1812
1813    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1814        && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1815    {
1816        point.column += ch.len_utf8() as u32;
1817    }
1818    for _ in 0..times {
1819        let new_point = movement::find_preceding_boundary_point(
1820            &map.buffer_snapshot,
1821            point,
1822            FindRange::MultiLine,
1823            |left, right| {
1824                let left_kind = classifier.kind(left);
1825                let right_kind = classifier.kind(right);
1826                match (left_kind, right_kind) {
1827                    (CharKind::Punctuation, CharKind::Whitespace)
1828                    | (CharKind::Punctuation, CharKind::Word)
1829                    | (CharKind::Word, CharKind::Whitespace)
1830                    | (CharKind::Word, CharKind::Punctuation) => true,
1831                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1832                    _ => false,
1833                }
1834            },
1835        );
1836        if new_point == point {
1837            break;
1838        }
1839        point = new_point;
1840    }
1841    movement::saturating_left(map, point.to_display_point(map))
1842}
1843
1844fn next_subword_start(
1845    map: &DisplaySnapshot,
1846    mut point: DisplayPoint,
1847    ignore_punctuation: bool,
1848    times: usize,
1849) -> DisplayPoint {
1850    let classifier = map
1851        .buffer_snapshot
1852        .char_classifier_at(point.to_point(map))
1853        .ignore_punctuation(ignore_punctuation);
1854    for _ in 0..times {
1855        let mut crossed_newline = false;
1856        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1857            let left_kind = classifier.kind(left);
1858            let right_kind = classifier.kind(right);
1859            let at_newline = right == '\n';
1860
1861            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1862            let is_subword_start =
1863                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1864
1865            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1866                || at_newline && crossed_newline
1867                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1868
1869            crossed_newline |= at_newline;
1870            found
1871        });
1872        if point == new_point {
1873            break;
1874        }
1875        point = new_point;
1876    }
1877    point
1878}
1879
1880pub(crate) fn next_subword_end(
1881    map: &DisplaySnapshot,
1882    mut point: DisplayPoint,
1883    ignore_punctuation: bool,
1884    times: usize,
1885    allow_cross_newline: bool,
1886) -> DisplayPoint {
1887    let classifier = map
1888        .buffer_snapshot
1889        .char_classifier_at(point.to_point(map))
1890        .ignore_punctuation(ignore_punctuation);
1891    for _ in 0..times {
1892        let new_point = next_char(map, point, allow_cross_newline);
1893
1894        let mut crossed_newline = false;
1895        let mut need_backtrack = false;
1896        let new_point =
1897            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1898                let left_kind = classifier.kind(left);
1899                let right_kind = classifier.kind(right);
1900                let at_newline = right == '\n';
1901
1902                if !allow_cross_newline && at_newline {
1903                    return true;
1904                }
1905
1906                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1907                let is_subword_end =
1908                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1909
1910                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1911
1912                if found && (is_word_end || is_subword_end) {
1913                    need_backtrack = true;
1914                }
1915
1916                crossed_newline |= at_newline;
1917                found
1918            });
1919        let mut new_point = map.clip_point(new_point, Bias::Left);
1920        if need_backtrack {
1921            *new_point.column_mut() -= 1;
1922        }
1923        let new_point = map.clip_point(new_point, Bias::Left);
1924        if point == new_point {
1925            break;
1926        }
1927        point = new_point;
1928    }
1929    point
1930}
1931
1932fn previous_subword_start(
1933    map: &DisplaySnapshot,
1934    mut point: DisplayPoint,
1935    ignore_punctuation: bool,
1936    times: usize,
1937) -> DisplayPoint {
1938    let classifier = map
1939        .buffer_snapshot
1940        .char_classifier_at(point.to_point(map))
1941        .ignore_punctuation(ignore_punctuation);
1942    for _ in 0..times {
1943        let mut crossed_newline = false;
1944        // This works even though find_preceding_boundary is called for every character in the line containing
1945        // cursor because the newline is checked only once.
1946        let new_point = movement::find_preceding_boundary_display_point(
1947            map,
1948            point,
1949            FindRange::MultiLine,
1950            |left, right| {
1951                let left_kind = classifier.kind(left);
1952                let right_kind = classifier.kind(right);
1953                let at_newline = right == '\n';
1954
1955                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1956                let is_subword_start =
1957                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1958
1959                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1960                    || at_newline && crossed_newline
1961                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1962
1963                crossed_newline |= at_newline;
1964
1965                found
1966            },
1967        );
1968        if point == new_point {
1969            break;
1970        }
1971        point = new_point;
1972    }
1973    point
1974}
1975
1976fn previous_subword_end(
1977    map: &DisplaySnapshot,
1978    point: DisplayPoint,
1979    ignore_punctuation: bool,
1980    times: usize,
1981) -> DisplayPoint {
1982    let classifier = map
1983        .buffer_snapshot
1984        .char_classifier_at(point.to_point(map))
1985        .ignore_punctuation(ignore_punctuation);
1986    let mut point = point.to_point(map);
1987
1988    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1989        && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1990    {
1991        point.column += ch.len_utf8() as u32;
1992    }
1993    for _ in 0..times {
1994        let new_point = movement::find_preceding_boundary_point(
1995            &map.buffer_snapshot,
1996            point,
1997            FindRange::MultiLine,
1998            |left, right| {
1999                let left_kind = classifier.kind(left);
2000                let right_kind = classifier.kind(right);
2001
2002                let is_subword_end =
2003                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
2004
2005                if is_subword_end {
2006                    return true;
2007                }
2008
2009                match (left_kind, right_kind) {
2010                    (CharKind::Word, CharKind::Whitespace)
2011                    | (CharKind::Word, CharKind::Punctuation) => true,
2012                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2013                    _ => false,
2014                }
2015            },
2016        );
2017        if new_point == point {
2018            break;
2019        }
2020        point = new_point;
2021    }
2022    movement::saturating_left(map, point.to_display_point(map))
2023}
2024
2025pub(crate) fn first_non_whitespace(
2026    map: &DisplaySnapshot,
2027    display_lines: bool,
2028    from: DisplayPoint,
2029) -> DisplayPoint {
2030    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2031    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2032    for (ch, offset) in map.buffer_chars_at(start_offset) {
2033        if ch == '\n' {
2034            return from;
2035        }
2036
2037        start_offset = offset;
2038
2039        if classifier.kind(ch) != CharKind::Whitespace {
2040            break;
2041        }
2042    }
2043
2044    start_offset.to_display_point(map)
2045}
2046
2047pub(crate) fn last_non_whitespace(
2048    map: &DisplaySnapshot,
2049    from: DisplayPoint,
2050    count: usize,
2051) -> DisplayPoint {
2052    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2053    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2054
2055    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2056    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2057        && classifier.kind(ch) != CharKind::Whitespace
2058    {
2059        return end_of_line.to_display_point(map);
2060    }
2061
2062    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2063        if ch == '\n' {
2064            break;
2065        }
2066        end_of_line = offset;
2067        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2068            break;
2069        }
2070    }
2071
2072    end_of_line.to_display_point(map)
2073}
2074
2075pub(crate) fn start_of_line(
2076    map: &DisplaySnapshot,
2077    display_lines: bool,
2078    point: DisplayPoint,
2079) -> DisplayPoint {
2080    if display_lines {
2081        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2082    } else {
2083        map.prev_line_boundary(point.to_point(map)).1
2084    }
2085}
2086
2087pub(crate) fn middle_of_line(
2088    map: &DisplaySnapshot,
2089    display_lines: bool,
2090    point: DisplayPoint,
2091    times: Option<usize>,
2092) -> DisplayPoint {
2093    let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2094        times as f64 / 100.
2095    } else {
2096        0.5
2097    };
2098    if display_lines {
2099        map.clip_point(
2100            DisplayPoint::new(
2101                point.row(),
2102                (map.line_len(point.row()) as f64 * percent) as u32,
2103            ),
2104            Bias::Left,
2105        )
2106    } else {
2107        let mut buffer_point = point.to_point(map);
2108        buffer_point.column = (map
2109            .buffer_snapshot
2110            .line_len(MultiBufferRow(buffer_point.row)) as f64
2111            * percent) as u32;
2112
2113        map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2114    }
2115}
2116
2117pub(crate) fn end_of_line(
2118    map: &DisplaySnapshot,
2119    display_lines: bool,
2120    mut point: DisplayPoint,
2121    times: usize,
2122) -> DisplayPoint {
2123    if times > 1 {
2124        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2125    }
2126    if display_lines {
2127        map.clip_point(
2128            DisplayPoint::new(point.row(), map.line_len(point.row())),
2129            Bias::Left,
2130        )
2131    } else {
2132        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2133    }
2134}
2135
2136pub(crate) fn sentence_backwards(
2137    map: &DisplaySnapshot,
2138    point: DisplayPoint,
2139    mut times: usize,
2140) -> DisplayPoint {
2141    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2142    let mut chars = map.reverse_buffer_chars_at(start).peekable();
2143
2144    let mut was_newline = map
2145        .buffer_chars_at(start)
2146        .next()
2147        .is_some_and(|(c, _)| c == '\n');
2148
2149    while let Some((ch, offset)) = chars.next() {
2150        let start_of_next_sentence = if was_newline && ch == '\n' {
2151            Some(offset + ch.len_utf8())
2152        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2153            Some(next_non_blank(map, offset + ch.len_utf8()))
2154        } else if ch == '.' || ch == '?' || ch == '!' {
2155            start_of_next_sentence(map, offset + ch.len_utf8())
2156        } else {
2157            None
2158        };
2159
2160        if let Some(start_of_next_sentence) = start_of_next_sentence {
2161            if start_of_next_sentence < start {
2162                times = times.saturating_sub(1);
2163            }
2164            if times == 0 || offset == 0 {
2165                return map.clip_point(
2166                    start_of_next_sentence
2167                        .to_offset(&map.buffer_snapshot)
2168                        .to_display_point(map),
2169                    Bias::Left,
2170                );
2171            }
2172        }
2173        if was_newline {
2174            start = offset;
2175        }
2176        was_newline = ch == '\n';
2177    }
2178
2179    DisplayPoint::zero()
2180}
2181
2182pub(crate) fn sentence_forwards(
2183    map: &DisplaySnapshot,
2184    point: DisplayPoint,
2185    mut times: usize,
2186) -> DisplayPoint {
2187    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2188    let mut chars = map.buffer_chars_at(start).peekable();
2189
2190    let mut was_newline = map
2191        .reverse_buffer_chars_at(start)
2192        .next()
2193        .is_some_and(|(c, _)| c == '\n')
2194        && chars.peek().is_some_and(|(c, _)| *c == '\n');
2195
2196    while let Some((ch, offset)) = chars.next() {
2197        if was_newline && ch == '\n' {
2198            continue;
2199        }
2200        let start_of_next_sentence = if was_newline {
2201            Some(next_non_blank(map, offset))
2202        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2203            Some(next_non_blank(map, offset + ch.len_utf8()))
2204        } else if ch == '.' || ch == '?' || ch == '!' {
2205            start_of_next_sentence(map, offset + ch.len_utf8())
2206        } else {
2207            None
2208        };
2209
2210        if let Some(start_of_next_sentence) = start_of_next_sentence {
2211            times = times.saturating_sub(1);
2212            if times == 0 {
2213                return map.clip_point(
2214                    start_of_next_sentence
2215                        .to_offset(&map.buffer_snapshot)
2216                        .to_display_point(map),
2217                    Bias::Right,
2218                );
2219            }
2220        }
2221
2222        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2223    }
2224
2225    map.max_point()
2226}
2227
2228fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2229    for (c, o) in map.buffer_chars_at(start) {
2230        if c == '\n' || !c.is_whitespace() {
2231            return o;
2232        }
2233    }
2234
2235    map.buffer_snapshot.len()
2236}
2237
2238// given the offset after a ., !, or ? find the start of the next sentence.
2239// if this is not a sentence boundary, returns None.
2240fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2241    let chars = map.buffer_chars_at(end_of_sentence);
2242    let mut seen_space = false;
2243
2244    for (char, offset) in chars {
2245        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2246            continue;
2247        }
2248
2249        if char == '\n' && seen_space {
2250            return Some(offset);
2251        } else if char.is_whitespace() {
2252            seen_space = true;
2253        } else if seen_space {
2254            return Some(offset);
2255        } else {
2256            return None;
2257        }
2258    }
2259
2260    Some(map.buffer_snapshot.len())
2261}
2262
2263fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2264    let point = map.display_point_to_point(display_point, Bias::Left);
2265    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2266        return display_point;
2267    };
2268    let offset = excerpt.buffer().point_to_offset(
2269        excerpt
2270            .buffer()
2271            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2272    );
2273    let buffer_range = excerpt.buffer_range();
2274    if offset >= buffer_range.start && offset <= buffer_range.end {
2275        let point = map
2276            .buffer_snapshot
2277            .offset_to_point(excerpt.map_offset_from_buffer(offset));
2278        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2279    }
2280    let mut last_position = None;
2281    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2282        let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
2283            ..language::ToOffset::to_offset(&range.context.end, buffer);
2284        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2285            let text_anchor = buffer.anchor_after(offset);
2286            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2287            return anchor.to_display_point(map);
2288        } else if offset <= excerpt_range.start {
2289            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2290            return anchor.to_display_point(map);
2291        } else {
2292            last_position = Some(Anchor::in_buffer(
2293                excerpt,
2294                buffer.remote_id(),
2295                range.context.end,
2296            ));
2297        }
2298    }
2299
2300    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2301    last_point.column = point.column;
2302
2303    map.clip_point(
2304        map.point_to_display_point(
2305            map.buffer_snapshot.clip_point(point, Bias::Left),
2306            Bias::Left,
2307        ),
2308        Bias::Left,
2309    )
2310}
2311
2312fn start_of_document(
2313    map: &DisplaySnapshot,
2314    display_point: DisplayPoint,
2315    maybe_times: Option<usize>,
2316) -> DisplayPoint {
2317    if let Some(times) = maybe_times {
2318        return go_to_line(map, display_point, times);
2319    }
2320
2321    let point = map.display_point_to_point(display_point, Bias::Left);
2322    let mut first_point = Point::zero();
2323    first_point.column = point.column;
2324
2325    map.clip_point(
2326        map.point_to_display_point(
2327            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2328            Bias::Left,
2329        ),
2330        Bias::Left,
2331    )
2332}
2333
2334fn end_of_document(
2335    map: &DisplaySnapshot,
2336    display_point: DisplayPoint,
2337    maybe_times: Option<usize>,
2338) -> DisplayPoint {
2339    if let Some(times) = maybe_times {
2340        return go_to_line(map, display_point, times);
2341    };
2342    let point = map.display_point_to_point(display_point, Bias::Left);
2343    let mut last_point = map.buffer_snapshot.max_point();
2344    last_point.column = point.column;
2345
2346    map.clip_point(
2347        map.point_to_display_point(
2348            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2349            Bias::Left,
2350        ),
2351        Bias::Left,
2352    )
2353}
2354
2355fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2356    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2357    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2358
2359    if head > outer.start && head < inner.start {
2360        let mut offset = inner.end.to_offset(map, Bias::Left);
2361        for c in map.buffer_snapshot.chars_at(offset) {
2362            if c == '/' || c == '\n' || c == '>' {
2363                return Some(offset.to_display_point(map));
2364            }
2365            offset += c.len_utf8();
2366        }
2367    } else {
2368        let mut offset = outer.start.to_offset(map, Bias::Left);
2369        for c in map.buffer_snapshot.chars_at(offset) {
2370            offset += c.len_utf8();
2371            if c == '<' || c == '\n' {
2372                return Some(offset.to_display_point(map));
2373            }
2374        }
2375    }
2376
2377    None
2378}
2379
2380fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2381    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2382    let display_point = map.clip_at_line_end(display_point);
2383    let point = display_point.to_point(map);
2384    let offset = point.to_offset(&map.buffer_snapshot);
2385
2386    // Ensure the range is contained by the current line.
2387    let mut line_end = map.next_line_boundary(point).0;
2388    if line_end == point {
2389        line_end = map.max_point().to_point(map);
2390    }
2391
2392    if let Some((opening_range, closing_range)) = map
2393        .buffer_snapshot
2394        .innermost_enclosing_bracket_ranges(offset..offset, None)
2395    {
2396        if opening_range.contains(&offset) {
2397            return closing_range.start.to_display_point(map);
2398        } else if closing_range.contains(&offset) {
2399            return opening_range.start.to_display_point(map);
2400        }
2401    }
2402
2403    let line_range = map.prev_line_boundary(point).0..line_end;
2404    let visible_line_range =
2405        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2406    let ranges = map.buffer_snapshot.bracket_ranges(visible_line_range);
2407    if let Some(ranges) = ranges {
2408        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2409            ..line_range.end.to_offset(&map.buffer_snapshot);
2410        let mut closest_pair_destination = None;
2411        let mut closest_distance = usize::MAX;
2412
2413        for (open_range, close_range) in ranges {
2414            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2415                if offset > open_range.start && offset < close_range.start {
2416                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2417                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2418                        return display_point;
2419                    }
2420                    if let Some(tag) = matching_tag(map, display_point) {
2421                        return tag;
2422                    }
2423                } else if close_range.contains(&offset) {
2424                    return open_range.start.to_display_point(map);
2425                } else if open_range.contains(&offset) {
2426                    return (close_range.end - 1).to_display_point(map);
2427                }
2428            }
2429
2430            if (open_range.contains(&offset) || open_range.start >= offset)
2431                && line_range.contains(&open_range.start)
2432            {
2433                let distance = open_range.start.saturating_sub(offset);
2434                if distance < closest_distance {
2435                    closest_pair_destination = Some(close_range.start);
2436                    closest_distance = distance;
2437                    continue;
2438                }
2439            }
2440
2441            if (close_range.contains(&offset) || close_range.start >= offset)
2442                && line_range.contains(&close_range.start)
2443            {
2444                let distance = close_range.start.saturating_sub(offset);
2445                if distance < closest_distance {
2446                    closest_pair_destination = Some(open_range.start);
2447                    closest_distance = distance;
2448                    continue;
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 = start_of_relative_buffer_row(map, 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 = start_of_relative_buffer_row(map, 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 = start_of_relative_buffer_row(map, 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 = start_of_relative_buffer_row(map, 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::display_map::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_comma_semicolon(cx: &mut gpui::TestAppContext) {
3390        let mut cx = NeovimBackedTestContext::new(cx).await;
3391
3392        // f and F
3393        cx.set_shared_state("ˇone two three four").await;
3394        cx.simulate_shared_keystrokes("f o").await;
3395        cx.shared_state().await.assert_eq("one twˇo three four");
3396        cx.simulate_shared_keystrokes(",").await;
3397        cx.shared_state().await.assert_eq("ˇone two three four");
3398        cx.simulate_shared_keystrokes("2 ;").await;
3399        cx.shared_state().await.assert_eq("one two three fˇour");
3400        cx.simulate_shared_keystrokes("shift-f e").await;
3401        cx.shared_state().await.assert_eq("one two threˇe four");
3402        cx.simulate_shared_keystrokes("2 ;").await;
3403        cx.shared_state().await.assert_eq("onˇe two three four");
3404        cx.simulate_shared_keystrokes(",").await;
3405        cx.shared_state().await.assert_eq("one two thrˇee four");
3406
3407        // t and T
3408        cx.set_shared_state("ˇone two three four").await;
3409        cx.simulate_shared_keystrokes("t o").await;
3410        cx.shared_state().await.assert_eq("one tˇwo three four");
3411        cx.simulate_shared_keystrokes(",").await;
3412        cx.shared_state().await.assert_eq("oˇne two three four");
3413        cx.simulate_shared_keystrokes("2 ;").await;
3414        cx.shared_state().await.assert_eq("one two three ˇfour");
3415        cx.simulate_shared_keystrokes("shift-t e").await;
3416        cx.shared_state().await.assert_eq("one two threeˇ four");
3417        cx.simulate_shared_keystrokes("3 ;").await;
3418        cx.shared_state().await.assert_eq("oneˇ two three four");
3419        cx.simulate_shared_keystrokes(",").await;
3420        cx.shared_state().await.assert_eq("one two thˇree four");
3421    }
3422
3423    #[gpui::test]
3424    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3425        let mut cx = NeovimBackedTestContext::new(cx).await;
3426        let initial_state = indoc! {r"something(ˇfoo)"};
3427        cx.set_shared_state(initial_state).await;
3428        cx.simulate_shared_keystrokes("}").await;
3429        cx.shared_state().await.assert_eq("something(fooˇ)");
3430    }
3431
3432    #[gpui::test]
3433    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3434        let mut cx = NeovimBackedTestContext::new(cx).await;
3435        cx.set_shared_state("ˇone\n  two\nthree").await;
3436        cx.simulate_shared_keystrokes("enter").await;
3437        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3438    }
3439
3440    #[gpui::test]
3441    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3442        let mut cx = NeovimBackedTestContext::new(cx).await;
3443        cx.set_shared_state("ˇ one\n two \nthree").await;
3444        cx.simulate_shared_keystrokes("g _").await;
3445        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3446
3447        cx.set_shared_state("ˇ one \n two \nthree").await;
3448        cx.simulate_shared_keystrokes("g _").await;
3449        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3450        cx.simulate_shared_keystrokes("2 g _").await;
3451        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3452    }
3453
3454    #[gpui::test]
3455    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3456        let mut cx = NeovimBackedTestContext::new(cx).await;
3457        let initial_state = indoc! {r"abc
3458          def
3459          paragraph
3460          the second
3461          third ˇand
3462          final"};
3463
3464        cx.set_shared_state(initial_state).await;
3465        cx.simulate_shared_keystrokes("shift-h").await;
3466        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3467          def
3468          paragraph
3469          the second
3470          third and
3471          final"});
3472
3473        // clip point
3474        cx.set_shared_state(indoc! {r"
3475          1 2 3
3476          4 5 6
3477          7 8 ˇ9
3478          "})
3479            .await;
3480        cx.simulate_shared_keystrokes("shift-h").await;
3481        cx.shared_state().await.assert_eq(indoc! {"
3482          1 2 ˇ3
3483          4 5 6
3484          7 8 9
3485          "});
3486
3487        cx.set_shared_state(indoc! {r"
3488          1 2 3
3489          4 5 6
3490          ˇ7 8 9
3491          "})
3492            .await;
3493        cx.simulate_shared_keystrokes("shift-h").await;
3494        cx.shared_state().await.assert_eq(indoc! {"
3495          ˇ1 2 3
3496          4 5 6
3497          7 8 9
3498          "});
3499
3500        cx.set_shared_state(indoc! {r"
3501          1 2 3
3502          4 5 ˇ6
3503          7 8 9"})
3504            .await;
3505        cx.simulate_shared_keystrokes("9 shift-h").await;
3506        cx.shared_state().await.assert_eq(indoc! {"
3507          1 2 3
3508          4 5 6
3509          7 8 ˇ9"});
3510    }
3511
3512    #[gpui::test]
3513    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3514        let mut cx = NeovimBackedTestContext::new(cx).await;
3515        let initial_state = indoc! {r"abˇc
3516          def
3517          paragraph
3518          the second
3519          third and
3520          final"};
3521
3522        cx.set_shared_state(initial_state).await;
3523        cx.simulate_shared_keystrokes("shift-m").await;
3524        cx.shared_state().await.assert_eq(indoc! {r"abc
3525          def
3526          paˇragraph
3527          the second
3528          third and
3529          final"});
3530
3531        cx.set_shared_state(indoc! {r"
3532          1 2 3
3533          4 5 6
3534          7 8 ˇ9
3535          "})
3536            .await;
3537        cx.simulate_shared_keystrokes("shift-m").await;
3538        cx.shared_state().await.assert_eq(indoc! {"
3539          1 2 3
3540          4 5 ˇ6
3541          7 8 9
3542          "});
3543        cx.set_shared_state(indoc! {r"
3544          1 2 3
3545          4 5 6
3546          ˇ7 8 9
3547          "})
3548            .await;
3549        cx.simulate_shared_keystrokes("shift-m").await;
3550        cx.shared_state().await.assert_eq(indoc! {"
3551          1 2 3
3552          ˇ4 5 6
3553          7 8 9
3554          "});
3555        cx.set_shared_state(indoc! {r"
3556          ˇ1 2 3
3557          4 5 6
3558          7 8 9
3559          "})
3560            .await;
3561        cx.simulate_shared_keystrokes("shift-m").await;
3562        cx.shared_state().await.assert_eq(indoc! {"
3563          1 2 3
3564          ˇ4 5 6
3565          7 8 9
3566          "});
3567        cx.set_shared_state(indoc! {r"
3568          1 2 3
3569          ˇ4 5 6
3570          7 8 9
3571          "})
3572            .await;
3573        cx.simulate_shared_keystrokes("shift-m").await;
3574        cx.shared_state().await.assert_eq(indoc! {"
3575          1 2 3
3576          ˇ4 5 6
3577          7 8 9
3578          "});
3579        cx.set_shared_state(indoc! {r"
3580          1 2 3
3581          4 5 ˇ6
3582          7 8 9
3583          "})
3584            .await;
3585        cx.simulate_shared_keystrokes("shift-m").await;
3586        cx.shared_state().await.assert_eq(indoc! {"
3587          1 2 3
3588          4 5 ˇ6
3589          7 8 9
3590          "});
3591    }
3592
3593    #[gpui::test]
3594    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3595        let mut cx = NeovimBackedTestContext::new(cx).await;
3596        let initial_state = indoc! {r"abc
3597          deˇf
3598          paragraph
3599          the second
3600          third and
3601          final"};
3602
3603        cx.set_shared_state(initial_state).await;
3604        cx.simulate_shared_keystrokes("shift-l").await;
3605        cx.shared_state().await.assert_eq(indoc! {r"abc
3606          def
3607          paragraph
3608          the second
3609          third and
3610          fiˇnal"});
3611
3612        cx.set_shared_state(indoc! {r"
3613          1 2 3
3614          4 5 ˇ6
3615          7 8 9
3616          "})
3617            .await;
3618        cx.simulate_shared_keystrokes("shift-l").await;
3619        cx.shared_state().await.assert_eq(indoc! {"
3620          1 2 3
3621          4 5 6
3622          7 8 9
3623          ˇ"});
3624
3625        cx.set_shared_state(indoc! {r"
3626          1 2 3
3627          ˇ4 5 6
3628          7 8 9
3629          "})
3630            .await;
3631        cx.simulate_shared_keystrokes("shift-l").await;
3632        cx.shared_state().await.assert_eq(indoc! {"
3633          1 2 3
3634          4 5 6
3635          7 8 9
3636          ˇ"});
3637
3638        cx.set_shared_state(indoc! {r"
3639          1 2 ˇ3
3640          4 5 6
3641          7 8 9
3642          "})
3643            .await;
3644        cx.simulate_shared_keystrokes("shift-l").await;
3645        cx.shared_state().await.assert_eq(indoc! {"
3646          1 2 3
3647          4 5 6
3648          7 8 9
3649          ˇ"});
3650
3651        cx.set_shared_state(indoc! {r"
3652          ˇ1 2 3
3653          4 5 6
3654          7 8 9
3655          "})
3656            .await;
3657        cx.simulate_shared_keystrokes("shift-l").await;
3658        cx.shared_state().await.assert_eq(indoc! {"
3659          1 2 3
3660          4 5 6
3661          7 8 9
3662          ˇ"});
3663
3664        cx.set_shared_state(indoc! {r"
3665          1 2 3
3666          4 5 ˇ6
3667          7 8 9
3668          "})
3669            .await;
3670        cx.simulate_shared_keystrokes("9 shift-l").await;
3671        cx.shared_state().await.assert_eq(indoc! {"
3672          1 2 ˇ3
3673          4 5 6
3674          7 8 9
3675          "});
3676    }
3677
3678    #[gpui::test]
3679    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3680        let mut cx = NeovimBackedTestContext::new(cx).await;
3681        cx.set_shared_state(indoc! {r"
3682        456 5ˇ67 678
3683        "})
3684            .await;
3685        cx.simulate_shared_keystrokes("g e").await;
3686        cx.shared_state().await.assert_eq(indoc! {"
3687        45ˇ6 567 678
3688        "});
3689
3690        // Test times
3691        cx.set_shared_state(indoc! {r"
3692        123 234 345
3693        456 5ˇ67 678
3694        "})
3695            .await;
3696        cx.simulate_shared_keystrokes("4 g e").await;
3697        cx.shared_state().await.assert_eq(indoc! {"
3698        12ˇ3 234 345
3699        456 567 678
3700        "});
3701
3702        // With punctuation
3703        cx.set_shared_state(indoc! {r"
3704        123 234 345
3705        4;5.6 5ˇ67 678
3706        789 890 901
3707        "})
3708            .await;
3709        cx.simulate_shared_keystrokes("g e").await;
3710        cx.shared_state().await.assert_eq(indoc! {"
3711          123 234 345
3712          4;5.ˇ6 567 678
3713          789 890 901
3714        "});
3715
3716        // With punctuation and count
3717        cx.set_shared_state(indoc! {r"
3718        123 234 345
3719        4;5.6 5ˇ67 678
3720        789 890 901
3721        "})
3722            .await;
3723        cx.simulate_shared_keystrokes("5 g e").await;
3724        cx.shared_state().await.assert_eq(indoc! {"
3725          123 234 345
3726          ˇ4;5.6 567 678
3727          789 890 901
3728        "});
3729
3730        // newlines
3731        cx.set_shared_state(indoc! {r"
3732        123 234 345
3733
3734        78ˇ9 890 901
3735        "})
3736            .await;
3737        cx.simulate_shared_keystrokes("g e").await;
3738        cx.shared_state().await.assert_eq(indoc! {"
3739          123 234 345
3740          ˇ
3741          789 890 901
3742        "});
3743        cx.simulate_shared_keystrokes("g e").await;
3744        cx.shared_state().await.assert_eq(indoc! {"
3745          123 234 34ˇ5
3746
3747          789 890 901
3748        "});
3749
3750        // With punctuation
3751        cx.set_shared_state(indoc! {r"
3752        123 234 345
3753        4;5.ˇ6 567 678
3754        789 890 901
3755        "})
3756            .await;
3757        cx.simulate_shared_keystrokes("g shift-e").await;
3758        cx.shared_state().await.assert_eq(indoc! {"
3759          123 234 34ˇ5
3760          4;5.6 567 678
3761          789 890 901
3762        "});
3763
3764        // With multi byte char
3765        cx.set_shared_state(indoc! {r"
3766        bar ˇó
3767        "})
3768            .await;
3769        cx.simulate_shared_keystrokes("g e").await;
3770        cx.shared_state().await.assert_eq(indoc! {"
3771        baˇr ó
3772        "});
3773    }
3774
3775    #[gpui::test]
3776    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3777        let mut cx = NeovimBackedTestContext::new(cx).await;
3778
3779        cx.set_shared_state(indoc! {"
3780            fn aˇ() {
3781              return
3782            }
3783        "})
3784            .await;
3785        cx.simulate_shared_keystrokes("v $ %").await;
3786        cx.shared_state().await.assert_eq(indoc! {"
3787            fn a«() {
3788              return
3789            }ˇ»
3790        "});
3791    }
3792
3793    #[gpui::test]
3794    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3795        let mut cx = VimTestContext::new(cx, true).await;
3796
3797        cx.set_state(
3798            indoc! {"
3799                struct Foo {
3800                ˇ
3801                }
3802            "},
3803            Mode::Normal,
3804        );
3805
3806        cx.update_editor(|editor, _window, cx| {
3807            let range = editor.selections.newest_anchor().range();
3808            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3809            let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
3810            editor.splice_inlays(&[], vec![inlay], cx);
3811        });
3812
3813        cx.simulate_keystrokes("j");
3814        cx.assert_state(
3815            indoc! {"
3816                struct Foo {
3817
3818                ˇ}
3819            "},
3820            Mode::Normal,
3821        );
3822    }
3823
3824    #[gpui::test]
3825    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3826        let mut cx = VimTestContext::new(cx, true).await;
3827
3828        cx.set_state(
3829            indoc! {"
3830            ˇstruct Foo {
3831
3832            }
3833        "},
3834            Mode::Normal,
3835        );
3836        cx.update_editor(|editor, _window, cx| {
3837            let snapshot = editor.buffer().read(cx).snapshot(cx);
3838            let end_of_line =
3839                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3840            let inlay_text = " hint";
3841            let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
3842            editor.splice_inlays(&[], vec![inlay], cx);
3843        });
3844        cx.simulate_keystrokes("$");
3845        cx.assert_state(
3846            indoc! {"
3847            struct Foo ˇ{
3848
3849            }
3850        "},
3851            Mode::Normal,
3852        );
3853    }
3854
3855    #[gpui::test]
3856    async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
3857        let mut cx = VimTestContext::new(cx, true).await;
3858
3859        // Test the exact scenario from issue #29134
3860        cx.set_state(
3861            indoc! {"
3862                fn main() {
3863                    let this_is_a_long_name = Vec::<u32>::new();
3864                    let new_oneˇ = this_is_a_long_name
3865                        .iter()
3866                        .map(|i| i + 1)
3867                        .map(|i| i * 2)
3868                        .collect::<Vec<_>>();
3869                }
3870            "},
3871            Mode::Normal,
3872        );
3873
3874        // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
3875        cx.update_editor(|editor, _window, cx| {
3876            let snapshot = editor.buffer().read(cx).snapshot(cx);
3877            // The empty line is at line 3 (0-indexed)
3878            let line_start = snapshot.anchor_after(Point::new(3, 0));
3879            let inlay_text = ": Vec<u32>";
3880            let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
3881            editor.splice_inlays(&[], vec![inlay], cx);
3882        });
3883
3884        // Enter visual mode
3885        cx.simulate_keystrokes("v");
3886        cx.assert_state(
3887            indoc! {"
3888                fn main() {
3889                    let this_is_a_long_name = Vec::<u32>::new();
3890                    let new_one« ˇ»= this_is_a_long_name
3891                        .iter()
3892                        .map(|i| i + 1)
3893                        .map(|i| i * 2)
3894                        .collect::<Vec<_>>();
3895                }
3896            "},
3897            Mode::Visual,
3898        );
3899
3900        // Move down - should go to the beginning of line 4, not skip to line 5
3901        cx.simulate_keystrokes("j");
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        // Test with multiple movements
3917        cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
3918
3919        // Add type hint on the empty line
3920        cx.update_editor(|editor, _window, cx| {
3921            let snapshot = editor.buffer().read(cx).snapshot(cx);
3922            let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
3923            let inlay_text = ": i32";
3924            let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
3925            editor.splice_inlays(&[], vec![inlay], cx);
3926        });
3927
3928        // Enter visual mode and move down twice
3929        cx.simulate_keystrokes("v j j");
3930        cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
3931    }
3932
3933    #[gpui::test]
3934    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3935        let mut cx = NeovimBackedTestContext::new(cx).await;
3936        // Normal mode
3937        cx.set_shared_state(indoc! {"
3938            The ˇquick brown
3939            fox jumps over
3940            the lazy dog
3941            The quick brown
3942            fox jumps over
3943            the lazy dog
3944            The quick brown
3945            fox jumps over
3946            the lazy dog"})
3947            .await;
3948        cx.simulate_shared_keystrokes("2 0 %").await;
3949        cx.shared_state().await.assert_eq(indoc! {"
3950            The quick brown
3951            fox ˇjumps over
3952            the lazy dog
3953            The quick brown
3954            fox jumps over
3955            the lazy dog
3956            The quick brown
3957            fox jumps over
3958            the lazy dog"});
3959
3960        cx.simulate_shared_keystrokes("2 5 %").await;
3961        cx.shared_state().await.assert_eq(indoc! {"
3962            The quick brown
3963            fox jumps over
3964            the ˇlazy dog
3965            The quick brown
3966            fox jumps over
3967            the lazy dog
3968            The quick brown
3969            fox jumps over
3970            the lazy dog"});
3971
3972        cx.simulate_shared_keystrokes("7 5 %").await;
3973        cx.shared_state().await.assert_eq(indoc! {"
3974            The quick brown
3975            fox jumps over
3976            the lazy dog
3977            The quick brown
3978            fox jumps over
3979            the lazy dog
3980            The ˇquick brown
3981            fox jumps over
3982            the lazy dog"});
3983
3984        // Visual mode
3985        cx.set_shared_state(indoc! {"
3986            The ˇquick brown
3987            fox jumps over
3988            the lazy dog
3989            The quick brown
3990            fox jumps over
3991            the lazy dog
3992            The quick brown
3993            fox jumps over
3994            the lazy dog"})
3995            .await;
3996        cx.simulate_shared_keystrokes("v 5 0 %").await;
3997        cx.shared_state().await.assert_eq(indoc! {"
3998            The «quick brown
3999            fox jumps over
4000            the lazy dog
4001            The quick brown
4002            fox jˇ»umps over
4003            the lazy dog
4004            The quick brown
4005            fox jumps over
4006            the lazy dog"});
4007
4008        cx.set_shared_state(indoc! {"
4009            The ˇquick brown
4010            fox jumps over
4011            the lazy dog
4012            The quick brown
4013            fox jumps over
4014            the lazy dog
4015            The quick brown
4016            fox jumps over
4017            the lazy dog"})
4018            .await;
4019        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4020        cx.shared_state().await.assert_eq(indoc! {"
4021            The «quick brown
4022            fox jumps over
4023            the lazy dog
4024            The quick brown
4025            fox jumps over
4026            the lazy dog
4027            The quick brown
4028            fox jumps over
4029            the lˇ»azy dog"});
4030    }
4031
4032    #[gpui::test]
4033    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4034        let mut cx = NeovimBackedTestContext::new(cx).await;
4035
4036        cx.set_shared_state("ˇπππππ").await;
4037        cx.simulate_shared_keystrokes("3 space").await;
4038        cx.shared_state().await.assert_eq("πππˇππ");
4039    }
4040
4041    #[gpui::test]
4042    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4043        let mut cx = NeovimBackedTestContext::new(cx).await;
4044
4045        cx.set_shared_state(indoc! {"
4046            ππππˇπ
4047            πanotherline"})
4048            .await;
4049        cx.simulate_shared_keystrokes("4 space").await;
4050        cx.shared_state().await.assert_eq(indoc! {"
4051            πππππ
4052            πanˇotherline"});
4053    }
4054
4055    #[gpui::test]
4056    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4057        let mut cx = NeovimBackedTestContext::new(cx).await;
4058
4059        cx.set_shared_state(indoc! {"
4060                        ππππ
4061                        πanˇotherline"})
4062            .await;
4063        cx.simulate_shared_keystrokes("4 backspace").await;
4064        cx.shared_state().await.assert_eq(indoc! {"
4065                        πππˇπ
4066                        πanotherline"});
4067    }
4068
4069    #[gpui::test]
4070    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4071        let mut cx = VimTestContext::new(cx, true).await;
4072        cx.set_state(
4073            indoc! {
4074                "func empty(a string) bool {
4075                     ˇif a == \"\" {
4076                         return true
4077                     }
4078                     return false
4079                }"
4080            },
4081            Mode::Normal,
4082        );
4083        cx.simulate_keystrokes("[ -");
4084        cx.assert_state(
4085            indoc! {
4086                "ˇfunc empty(a string) bool {
4087                     if a == \"\" {
4088                         return true
4089                     }
4090                     return false
4091                }"
4092            },
4093            Mode::Normal,
4094        );
4095        cx.simulate_keystrokes("] =");
4096        cx.assert_state(
4097            indoc! {
4098                "func empty(a string) bool {
4099                     if a == \"\" {
4100                         return true
4101                     }
4102                     return false
4103                ˇ}"
4104            },
4105            Mode::Normal,
4106        );
4107        cx.simulate_keystrokes("[ +");
4108        cx.assert_state(
4109            indoc! {
4110                "func empty(a string) bool {
4111                     if a == \"\" {
4112                         return true
4113                     }
4114                     ˇreturn false
4115                }"
4116            },
4117            Mode::Normal,
4118        );
4119        cx.simulate_keystrokes("2 [ =");
4120        cx.assert_state(
4121            indoc! {
4122                "func empty(a string) bool {
4123                     ˇif a == \"\" {
4124                         return true
4125                     }
4126                     return false
4127                }"
4128            },
4129            Mode::Normal,
4130        );
4131        cx.simulate_keystrokes("] +");
4132        cx.assert_state(
4133            indoc! {
4134                "func empty(a string) bool {
4135                     if a == \"\" {
4136                         ˇreturn true
4137                     }
4138                     return false
4139                }"
4140            },
4141            Mode::Normal,
4142        );
4143        cx.simulate_keystrokes("] -");
4144        cx.assert_state(
4145            indoc! {
4146                "func empty(a string) bool {
4147                     if a == \"\" {
4148                         return true
4149                     ˇ}
4150                     return false
4151                }"
4152            },
4153            Mode::Normal,
4154        );
4155    }
4156
4157    #[gpui::test]
4158    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4159        let mut cx = NeovimBackedTestContext::new(cx).await;
4160        cx.set_shared_state("abˇc").await;
4161        cx.simulate_shared_keystrokes("delete").await;
4162        cx.shared_state().await.assert_eq("aˇb");
4163    }
4164
4165    #[gpui::test]
4166    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4167        let mut cx = NeovimBackedTestContext::new(cx).await;
4168
4169        cx.set_shared_state(indoc! {"
4170             ˇthe quick brown fox
4171             jumped over the lazy dog"})
4172            .await;
4173        cx.simulate_shared_keystrokes("d v 0").await;
4174        cx.shared_state().await.assert_eq(indoc! {"
4175             ˇhe quick brown fox
4176             jumped over the lazy dog"});
4177        assert!(!cx.cx.forced_motion());
4178
4179        cx.set_shared_state(indoc! {"
4180            the quick bˇrown fox
4181            jumped over the lazy dog"})
4182            .await;
4183        cx.simulate_shared_keystrokes("d v 0").await;
4184        cx.shared_state().await.assert_eq(indoc! {"
4185            ˇown fox
4186            jumped over the lazy dog"});
4187        assert!(!cx.cx.forced_motion());
4188
4189        cx.set_shared_state(indoc! {"
4190            the quick brown foˇx
4191            jumped over the lazy dog"})
4192            .await;
4193        cx.simulate_shared_keystrokes("d v 0").await;
4194        cx.shared_state().await.assert_eq(indoc! {"
4195            ˇ
4196            jumped over the lazy dog"});
4197        assert!(!cx.cx.forced_motion());
4198    }
4199
4200    #[gpui::test]
4201    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4202        let mut cx = NeovimBackedTestContext::new(cx).await;
4203
4204        cx.set_shared_state(indoc! {"
4205             ˇthe quick brown fox
4206             jumped over the lazy dog"})
4207            .await;
4208        cx.simulate_shared_keystrokes("d v g shift-m").await;
4209        cx.shared_state().await.assert_eq(indoc! {"
4210             ˇbrown fox
4211             jumped over the lazy dog"});
4212        assert!(!cx.cx.forced_motion());
4213
4214        cx.set_shared_state(indoc! {"
4215            the quick bˇrown fox
4216            jumped over the lazy dog"})
4217            .await;
4218        cx.simulate_shared_keystrokes("d v g shift-m").await;
4219        cx.shared_state().await.assert_eq(indoc! {"
4220            the quickˇown fox
4221            jumped over the lazy dog"});
4222        assert!(!cx.cx.forced_motion());
4223
4224        cx.set_shared_state(indoc! {"
4225            the quick brown foˇx
4226            jumped over the lazy dog"})
4227            .await;
4228        cx.simulate_shared_keystrokes("d v g shift-m").await;
4229        cx.shared_state().await.assert_eq(indoc! {"
4230            the quicˇk
4231            jumped over the lazy dog"});
4232        assert!(!cx.cx.forced_motion());
4233
4234        cx.set_shared_state(indoc! {"
4235            ˇthe quick brown fox
4236            jumped over the lazy dog"})
4237            .await;
4238        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4239        cx.shared_state().await.assert_eq(indoc! {"
4240            ˇ fox
4241            jumped over the lazy dog"});
4242        assert!(!cx.cx.forced_motion());
4243
4244        cx.set_shared_state(indoc! {"
4245            ˇthe quick brown fox
4246            jumped over the lazy dog"})
4247            .await;
4248        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4249        cx.shared_state().await.assert_eq(indoc! {"
4250            ˇuick brown fox
4251            jumped over the lazy dog"});
4252        assert!(!cx.cx.forced_motion());
4253    }
4254
4255    #[gpui::test]
4256    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4257        let mut cx = NeovimBackedTestContext::new(cx).await;
4258
4259        cx.set_shared_state(indoc! {"
4260             the quick brown foˇx
4261             jumped over the lazy dog"})
4262            .await;
4263        cx.simulate_shared_keystrokes("d v $").await;
4264        cx.shared_state().await.assert_eq(indoc! {"
4265             the quick brown foˇx
4266             jumped over the lazy dog"});
4267        assert!(!cx.cx.forced_motion());
4268
4269        cx.set_shared_state(indoc! {"
4270             ˇthe quick brown fox
4271             jumped over the lazy dog"})
4272            .await;
4273        cx.simulate_shared_keystrokes("d v $").await;
4274        cx.shared_state().await.assert_eq(indoc! {"
4275             ˇx
4276             jumped over the lazy dog"});
4277        assert!(!cx.cx.forced_motion());
4278    }
4279
4280    #[gpui::test]
4281    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4282        let mut cx = NeovimBackedTestContext::new(cx).await;
4283
4284        cx.set_shared_state(indoc! {"
4285               ˇthe quick brown fox
4286               jumped over the lazy dog"})
4287            .await;
4288        cx.simulate_shared_keystrokes("y v j p").await;
4289        cx.shared_state().await.assert_eq(indoc! {"
4290               the quick brown fox
4291               ˇthe quick brown fox
4292               jumped over the lazy dog"});
4293        assert!(!cx.cx.forced_motion());
4294
4295        cx.set_shared_state(indoc! {"
4296              the quick bˇrown fox
4297              jumped over the lazy dog"})
4298            .await;
4299        cx.simulate_shared_keystrokes("y v j p").await;
4300        cx.shared_state().await.assert_eq(indoc! {"
4301              the quick brˇrown fox
4302              jumped overown fox
4303              jumped over the lazy dog"});
4304        assert!(!cx.cx.forced_motion());
4305
4306        cx.set_shared_state(indoc! {"
4307             the quick brown foˇx
4308             jumped over the lazy dog"})
4309            .await;
4310        cx.simulate_shared_keystrokes("y v j p").await;
4311        cx.shared_state().await.assert_eq(indoc! {"
4312             the quick brown foxˇx
4313             jumped over the la
4314             jumped over the lazy dog"});
4315        assert!(!cx.cx.forced_motion());
4316
4317        cx.set_shared_state(indoc! {"
4318             the quick brown fox
4319             jˇumped over the lazy dog"})
4320            .await;
4321        cx.simulate_shared_keystrokes("y v k p").await;
4322        cx.shared_state().await.assert_eq(indoc! {"
4323            thˇhe quick brown fox
4324            je quick brown fox
4325            jumped over the lazy dog"});
4326        assert!(!cx.cx.forced_motion());
4327    }
4328
4329    #[gpui::test]
4330    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4331        let mut cx = NeovimBackedTestContext::new(cx).await;
4332
4333        cx.set_shared_state(indoc! {"
4334              ˇthe quick brown fox
4335              jumped over the lazy dog"})
4336            .await;
4337        cx.simulate_shared_keystrokes("d v e").await;
4338        cx.shared_state().await.assert_eq(indoc! {"
4339              ˇe quick brown fox
4340              jumped over the lazy dog"});
4341        assert!(!cx.cx.forced_motion());
4342
4343        cx.set_shared_state(indoc! {"
4344              the quick bˇrown fox
4345              jumped over the lazy dog"})
4346            .await;
4347        cx.simulate_shared_keystrokes("d v e").await;
4348        cx.shared_state().await.assert_eq(indoc! {"
4349              the quick bˇn fox
4350              jumped over the lazy dog"});
4351        assert!(!cx.cx.forced_motion());
4352
4353        cx.set_shared_state(indoc! {"
4354             the quick brown foˇx
4355             jumped over the lazy dog"})
4356            .await;
4357        cx.simulate_shared_keystrokes("d v e").await;
4358        cx.shared_state().await.assert_eq(indoc! {"
4359        the quick brown foˇd over the lazy dog"});
4360        assert!(!cx.cx.forced_motion());
4361    }
4362}