motion.rs

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