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
1528pub(crate) fn start_of_relative_buffer_row(
1529    map: &DisplaySnapshot,
1530    point: DisplayPoint,
1531    times: isize,
1532) -> DisplayPoint {
1533    let start = map.display_point_to_fold_point(point, Bias::Left);
1534    let target = start.row() as isize + times;
1535    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1536
1537    map.clip_point(
1538        map.fold_point_to_display_point(
1539            map.fold_snapshot
1540                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1541        ),
1542        Bias::Right,
1543    )
1544}
1545
1546fn up_down_buffer_rows(
1547    map: &DisplaySnapshot,
1548    mut point: DisplayPoint,
1549    mut goal: SelectionGoal,
1550    mut times: isize,
1551    text_layout_details: &TextLayoutDetails,
1552) -> (DisplayPoint, SelectionGoal) {
1553    let bias = if times < 0 { Bias::Left } else { Bias::Right };
1554
1555    while map.is_folded_buffer_header(point.row()) {
1556        if times < 0 {
1557            (point, _) = movement::up(map, point, goal, true, text_layout_details);
1558            times += 1;
1559        } else if times > 0 {
1560            (point, _) = movement::down(map, point, goal, true, text_layout_details);
1561            times -= 1;
1562        } else {
1563            break;
1564        }
1565    }
1566
1567    let start = map.display_point_to_fold_point(point, Bias::Left);
1568    let begin_folded_line = map.fold_point_to_display_point(
1569        map.fold_snapshot
1570            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1571    );
1572    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1573
1574    let (goal_wrap, goal_x) = match goal {
1575        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1576        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end as f32),
1577        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x as f32),
1578        _ => {
1579            let x = map.x_for_display_point(point, text_layout_details);
1580            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.into()));
1581            (select_nth_wrapped_row, x.into())
1582        }
1583    };
1584
1585    let target = start.row() as isize + times;
1586    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1587
1588    let mut begin_folded_line = map.fold_point_to_display_point(
1589        map.fold_snapshot
1590            .clip_point(FoldPoint::new(new_row, 0), bias),
1591    );
1592
1593    let mut i = 0;
1594    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1595        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1596        if map
1597            .display_point_to_fold_point(next_folded_line, bias)
1598            .row()
1599            == new_row
1600        {
1601            i += 1;
1602            begin_folded_line = next_folded_line;
1603        } else {
1604            break;
1605        }
1606    }
1607
1608    let new_col = if i == goal_wrap {
1609        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1610    } else {
1611        map.line_len(begin_folded_line.row())
1612    };
1613
1614    let point = DisplayPoint::new(begin_folded_line.row(), new_col);
1615    let mut clipped_point = map.clip_point(point, bias);
1616
1617    // When navigating vertically in vim mode with inlay hints present,
1618    // we need to handle the case where clipping moves us to a different row.
1619    // This can happen when moving down (Bias::Right) and hitting an inlay hint.
1620    // Re-clip with opposite bias to stay on the intended line.
1621    //
1622    // See: https://github.com/zed-industries/zed/issues/29134
1623    if clipped_point.row() > point.row() {
1624        clipped_point = map.clip_point(point, Bias::Left);
1625    }
1626
1627    (clipped_point, goal)
1628}
1629
1630fn down_display(
1631    map: &DisplaySnapshot,
1632    mut point: DisplayPoint,
1633    mut goal: SelectionGoal,
1634    times: usize,
1635    text_layout_details: &TextLayoutDetails,
1636) -> (DisplayPoint, SelectionGoal) {
1637    for _ in 0..times {
1638        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1639    }
1640
1641    (point, goal)
1642}
1643
1644fn up_display(
1645    map: &DisplaySnapshot,
1646    mut point: DisplayPoint,
1647    mut goal: SelectionGoal,
1648    times: usize,
1649    text_layout_details: &TextLayoutDetails,
1650) -> (DisplayPoint, SelectionGoal) {
1651    for _ in 0..times {
1652        (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1653    }
1654
1655    (point, goal)
1656}
1657
1658pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1659    for _ in 0..times {
1660        let new_point = movement::saturating_right(map, point);
1661        if point == new_point {
1662            break;
1663        }
1664        point = new_point;
1665    }
1666    point
1667}
1668
1669pub(crate) fn next_char(
1670    map: &DisplaySnapshot,
1671    point: DisplayPoint,
1672    allow_cross_newline: bool,
1673) -> DisplayPoint {
1674    let mut new_point = point;
1675    let mut max_column = map.line_len(new_point.row());
1676    if !allow_cross_newline {
1677        max_column -= 1;
1678    }
1679    if new_point.column() < max_column {
1680        *new_point.column_mut() += 1;
1681    } else if new_point < map.max_point() && allow_cross_newline {
1682        *new_point.row_mut() += 1;
1683        *new_point.column_mut() = 0;
1684    }
1685    map.clip_ignoring_line_ends(new_point, Bias::Right)
1686}
1687
1688pub(crate) fn next_word_start(
1689    map: &DisplaySnapshot,
1690    mut point: DisplayPoint,
1691    ignore_punctuation: bool,
1692    times: usize,
1693) -> DisplayPoint {
1694    let classifier = map
1695        .buffer_snapshot
1696        .char_classifier_at(point.to_point(map))
1697        .ignore_punctuation(ignore_punctuation);
1698    for _ in 0..times {
1699        let mut crossed_newline = false;
1700        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1701            let left_kind = classifier.kind(left);
1702            let right_kind = classifier.kind(right);
1703            let at_newline = right == '\n';
1704
1705            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1706                || at_newline && crossed_newline
1707                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1708
1709            crossed_newline |= at_newline;
1710            found
1711        });
1712        if point == new_point {
1713            break;
1714        }
1715        point = new_point;
1716    }
1717    point
1718}
1719
1720pub(crate) fn next_word_end(
1721    map: &DisplaySnapshot,
1722    mut point: DisplayPoint,
1723    ignore_punctuation: bool,
1724    times: usize,
1725    allow_cross_newline: bool,
1726    always_advance: bool,
1727) -> DisplayPoint {
1728    let classifier = map
1729        .buffer_snapshot
1730        .char_classifier_at(point.to_point(map))
1731        .ignore_punctuation(ignore_punctuation);
1732    for _ in 0..times {
1733        let mut need_next_char = false;
1734        let new_point = if always_advance {
1735            next_char(map, point, allow_cross_newline)
1736        } else {
1737            point
1738        };
1739        let new_point = movement::find_boundary_exclusive(
1740            map,
1741            new_point,
1742            FindRange::MultiLine,
1743            |left, right| {
1744                let left_kind = classifier.kind(left);
1745                let right_kind = classifier.kind(right);
1746                let at_newline = right == '\n';
1747
1748                if !allow_cross_newline && at_newline {
1749                    need_next_char = true;
1750                    return true;
1751                }
1752
1753                left_kind != right_kind && left_kind != CharKind::Whitespace
1754            },
1755        );
1756        let new_point = if need_next_char {
1757            next_char(map, new_point, true)
1758        } else {
1759            new_point
1760        };
1761        let new_point = map.clip_point(new_point, Bias::Left);
1762        if point == new_point {
1763            break;
1764        }
1765        point = new_point;
1766    }
1767    point
1768}
1769
1770fn previous_word_start(
1771    map: &DisplaySnapshot,
1772    mut point: DisplayPoint,
1773    ignore_punctuation: bool,
1774    times: usize,
1775) -> DisplayPoint {
1776    let classifier = map
1777        .buffer_snapshot
1778        .char_classifier_at(point.to_point(map))
1779        .ignore_punctuation(ignore_punctuation);
1780    for _ in 0..times {
1781        // This works even though find_preceding_boundary is called for every character in the line containing
1782        // cursor because the newline is checked only once.
1783        let new_point = movement::find_preceding_boundary_display_point(
1784            map,
1785            point,
1786            FindRange::MultiLine,
1787            |left, right| {
1788                let left_kind = classifier.kind(left);
1789                let right_kind = classifier.kind(right);
1790
1791                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1792            },
1793        );
1794        if point == new_point {
1795            break;
1796        }
1797        point = new_point;
1798    }
1799    point
1800}
1801
1802fn previous_word_end(
1803    map: &DisplaySnapshot,
1804    point: DisplayPoint,
1805    ignore_punctuation: bool,
1806    times: usize,
1807) -> DisplayPoint {
1808    let classifier = map
1809        .buffer_snapshot
1810        .char_classifier_at(point.to_point(map))
1811        .ignore_punctuation(ignore_punctuation);
1812    let mut point = point.to_point(map);
1813
1814    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1815        && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1816    {
1817        point.column += ch.len_utf8() as u32;
1818    }
1819    for _ in 0..times {
1820        let new_point = movement::find_preceding_boundary_point(
1821            &map.buffer_snapshot,
1822            point,
1823            FindRange::MultiLine,
1824            |left, right| {
1825                let left_kind = classifier.kind(left);
1826                let right_kind = classifier.kind(right);
1827                match (left_kind, right_kind) {
1828                    (CharKind::Punctuation, CharKind::Whitespace)
1829                    | (CharKind::Punctuation, CharKind::Word)
1830                    | (CharKind::Word, CharKind::Whitespace)
1831                    | (CharKind::Word, CharKind::Punctuation) => true,
1832                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1833                    _ => false,
1834                }
1835            },
1836        );
1837        if new_point == point {
1838            break;
1839        }
1840        point = new_point;
1841    }
1842    movement::saturating_left(map, point.to_display_point(map))
1843}
1844
1845fn next_subword_start(
1846    map: &DisplaySnapshot,
1847    mut point: DisplayPoint,
1848    ignore_punctuation: bool,
1849    times: usize,
1850) -> DisplayPoint {
1851    let classifier = map
1852        .buffer_snapshot
1853        .char_classifier_at(point.to_point(map))
1854        .ignore_punctuation(ignore_punctuation);
1855    for _ in 0..times {
1856        let mut crossed_newline = false;
1857        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1858            let left_kind = classifier.kind(left);
1859            let right_kind = classifier.kind(right);
1860            let at_newline = right == '\n';
1861
1862            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1863            let is_subword_start =
1864                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1865
1866            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1867                || at_newline && crossed_newline
1868                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1869
1870            crossed_newline |= at_newline;
1871            found
1872        });
1873        if point == new_point {
1874            break;
1875        }
1876        point = new_point;
1877    }
1878    point
1879}
1880
1881pub(crate) fn next_subword_end(
1882    map: &DisplaySnapshot,
1883    mut point: DisplayPoint,
1884    ignore_punctuation: bool,
1885    times: usize,
1886    allow_cross_newline: bool,
1887) -> DisplayPoint {
1888    let classifier = map
1889        .buffer_snapshot
1890        .char_classifier_at(point.to_point(map))
1891        .ignore_punctuation(ignore_punctuation);
1892    for _ in 0..times {
1893        let new_point = next_char(map, point, allow_cross_newline);
1894
1895        let mut crossed_newline = false;
1896        let mut need_backtrack = false;
1897        let new_point =
1898            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1899                let left_kind = classifier.kind(left);
1900                let right_kind = classifier.kind(right);
1901                let at_newline = right == '\n';
1902
1903                if !allow_cross_newline && at_newline {
1904                    return true;
1905                }
1906
1907                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1908                let is_subword_end =
1909                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1910
1911                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1912
1913                if found && (is_word_end || is_subword_end) {
1914                    need_backtrack = true;
1915                }
1916
1917                crossed_newline |= at_newline;
1918                found
1919            });
1920        let mut new_point = map.clip_point(new_point, Bias::Left);
1921        if need_backtrack {
1922            *new_point.column_mut() -= 1;
1923        }
1924        let new_point = map.clip_point(new_point, Bias::Left);
1925        if point == new_point {
1926            break;
1927        }
1928        point = new_point;
1929    }
1930    point
1931}
1932
1933fn previous_subword_start(
1934    map: &DisplaySnapshot,
1935    mut point: DisplayPoint,
1936    ignore_punctuation: bool,
1937    times: usize,
1938) -> DisplayPoint {
1939    let classifier = map
1940        .buffer_snapshot
1941        .char_classifier_at(point.to_point(map))
1942        .ignore_punctuation(ignore_punctuation);
1943    for _ in 0..times {
1944        let mut crossed_newline = false;
1945        // This works even though find_preceding_boundary is called for every character in the line containing
1946        // cursor because the newline is checked only once.
1947        let new_point = movement::find_preceding_boundary_display_point(
1948            map,
1949            point,
1950            FindRange::MultiLine,
1951            |left, right| {
1952                let left_kind = classifier.kind(left);
1953                let right_kind = classifier.kind(right);
1954                let at_newline = right == '\n';
1955
1956                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1957                let is_subword_start =
1958                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1959
1960                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1961                    || at_newline && crossed_newline
1962                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1963
1964                crossed_newline |= at_newline;
1965
1966                found
1967            },
1968        );
1969        if point == new_point {
1970            break;
1971        }
1972        point = new_point;
1973    }
1974    point
1975}
1976
1977fn previous_subword_end(
1978    map: &DisplaySnapshot,
1979    point: DisplayPoint,
1980    ignore_punctuation: bool,
1981    times: usize,
1982) -> DisplayPoint {
1983    let classifier = map
1984        .buffer_snapshot
1985        .char_classifier_at(point.to_point(map))
1986        .ignore_punctuation(ignore_punctuation);
1987    let mut point = point.to_point(map);
1988
1989    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1990        && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1991    {
1992        point.column += ch.len_utf8() as u32;
1993    }
1994    for _ in 0..times {
1995        let new_point = movement::find_preceding_boundary_point(
1996            &map.buffer_snapshot,
1997            point,
1998            FindRange::MultiLine,
1999            |left, right| {
2000                let left_kind = classifier.kind(left);
2001                let right_kind = classifier.kind(right);
2002
2003                let is_subword_end =
2004                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
2005
2006                if is_subword_end {
2007                    return true;
2008                }
2009
2010                match (left_kind, right_kind) {
2011                    (CharKind::Word, CharKind::Whitespace)
2012                    | (CharKind::Word, CharKind::Punctuation) => true,
2013                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2014                    _ => false,
2015                }
2016            },
2017        );
2018        if new_point == point {
2019            break;
2020        }
2021        point = new_point;
2022    }
2023    movement::saturating_left(map, point.to_display_point(map))
2024}
2025
2026pub(crate) fn first_non_whitespace(
2027    map: &DisplaySnapshot,
2028    display_lines: bool,
2029    from: DisplayPoint,
2030) -> DisplayPoint {
2031    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2032    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2033    for (ch, offset) in map.buffer_chars_at(start_offset) {
2034        if ch == '\n' {
2035            return from;
2036        }
2037
2038        start_offset = offset;
2039
2040        if classifier.kind(ch) != CharKind::Whitespace {
2041            break;
2042        }
2043    }
2044
2045    start_offset.to_display_point(map)
2046}
2047
2048pub(crate) fn last_non_whitespace(
2049    map: &DisplaySnapshot,
2050    from: DisplayPoint,
2051    count: usize,
2052) -> DisplayPoint {
2053    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2054    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2055
2056    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2057    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2058        && classifier.kind(ch) != CharKind::Whitespace
2059    {
2060        return end_of_line.to_display_point(map);
2061    }
2062
2063    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2064        if ch == '\n' {
2065            break;
2066        }
2067        end_of_line = offset;
2068        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2069            break;
2070        }
2071    }
2072
2073    end_of_line.to_display_point(map)
2074}
2075
2076pub(crate) fn start_of_line(
2077    map: &DisplaySnapshot,
2078    display_lines: bool,
2079    point: DisplayPoint,
2080) -> DisplayPoint {
2081    if display_lines {
2082        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2083    } else {
2084        map.prev_line_boundary(point.to_point(map)).1
2085    }
2086}
2087
2088pub(crate) fn middle_of_line(
2089    map: &DisplaySnapshot,
2090    display_lines: bool,
2091    point: DisplayPoint,
2092    times: Option<usize>,
2093) -> DisplayPoint {
2094    let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2095        times as f64 / 100.
2096    } else {
2097        0.5
2098    };
2099    if display_lines {
2100        map.clip_point(
2101            DisplayPoint::new(
2102                point.row(),
2103                (map.line_len(point.row()) as f64 * percent) as u32,
2104            ),
2105            Bias::Left,
2106        )
2107    } else {
2108        let mut buffer_point = point.to_point(map);
2109        buffer_point.column = (map
2110            .buffer_snapshot
2111            .line_len(MultiBufferRow(buffer_point.row)) as f64
2112            * percent) as u32;
2113
2114        map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2115    }
2116}
2117
2118pub(crate) fn end_of_line(
2119    map: &DisplaySnapshot,
2120    display_lines: bool,
2121    mut point: DisplayPoint,
2122    times: usize,
2123) -> DisplayPoint {
2124    if times > 1 {
2125        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2126    }
2127    if display_lines {
2128        map.clip_point(
2129            DisplayPoint::new(point.row(), map.line_len(point.row())),
2130            Bias::Left,
2131        )
2132    } else {
2133        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2134    }
2135}
2136
2137pub(crate) fn sentence_backwards(
2138    map: &DisplaySnapshot,
2139    point: DisplayPoint,
2140    mut times: usize,
2141) -> DisplayPoint {
2142    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2143    let mut chars = map.reverse_buffer_chars_at(start).peekable();
2144
2145    let mut was_newline = map
2146        .buffer_chars_at(start)
2147        .next()
2148        .is_some_and(|(c, _)| c == '\n');
2149
2150    while let Some((ch, offset)) = chars.next() {
2151        let start_of_next_sentence = if was_newline && ch == '\n' {
2152            Some(offset + ch.len_utf8())
2153        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2154            Some(next_non_blank(map, offset + ch.len_utf8()))
2155        } else if ch == '.' || ch == '?' || ch == '!' {
2156            start_of_next_sentence(map, offset + ch.len_utf8())
2157        } else {
2158            None
2159        };
2160
2161        if let Some(start_of_next_sentence) = start_of_next_sentence {
2162            if start_of_next_sentence < start {
2163                times = times.saturating_sub(1);
2164            }
2165            if times == 0 || offset == 0 {
2166                return map.clip_point(
2167                    start_of_next_sentence
2168                        .to_offset(&map.buffer_snapshot)
2169                        .to_display_point(map),
2170                    Bias::Left,
2171                );
2172            }
2173        }
2174        if was_newline {
2175            start = offset;
2176        }
2177        was_newline = ch == '\n';
2178    }
2179
2180    DisplayPoint::zero()
2181}
2182
2183pub(crate) fn sentence_forwards(
2184    map: &DisplaySnapshot,
2185    point: DisplayPoint,
2186    mut times: usize,
2187) -> DisplayPoint {
2188    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2189    let mut chars = map.buffer_chars_at(start).peekable();
2190
2191    let mut was_newline = map
2192        .reverse_buffer_chars_at(start)
2193        .next()
2194        .is_some_and(|(c, _)| c == '\n')
2195        && chars.peek().is_some_and(|(c, _)| *c == '\n');
2196
2197    while let Some((ch, offset)) = chars.next() {
2198        if was_newline && ch == '\n' {
2199            continue;
2200        }
2201        let start_of_next_sentence = if was_newline {
2202            Some(next_non_blank(map, offset))
2203        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2204            Some(next_non_blank(map, offset + ch.len_utf8()))
2205        } else if ch == '.' || ch == '?' || ch == '!' {
2206            start_of_next_sentence(map, offset + ch.len_utf8())
2207        } else {
2208            None
2209        };
2210
2211        if let Some(start_of_next_sentence) = start_of_next_sentence {
2212            times = times.saturating_sub(1);
2213            if times == 0 {
2214                return map.clip_point(
2215                    start_of_next_sentence
2216                        .to_offset(&map.buffer_snapshot)
2217                        .to_display_point(map),
2218                    Bias::Right,
2219                );
2220            }
2221        }
2222
2223        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2224    }
2225
2226    map.max_point()
2227}
2228
2229fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2230    for (c, o) in map.buffer_chars_at(start) {
2231        if c == '\n' || !c.is_whitespace() {
2232            return o;
2233        }
2234    }
2235
2236    map.buffer_snapshot.len()
2237}
2238
2239// given the offset after a ., !, or ? find the start of the next sentence.
2240// if this is not a sentence boundary, returns None.
2241fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2242    let chars = map.buffer_chars_at(end_of_sentence);
2243    let mut seen_space = false;
2244
2245    for (char, offset) in chars {
2246        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2247            continue;
2248        }
2249
2250        if char == '\n' && seen_space {
2251            return Some(offset);
2252        } else if char.is_whitespace() {
2253            seen_space = true;
2254        } else if seen_space {
2255            return Some(offset);
2256        } else {
2257            return None;
2258        }
2259    }
2260
2261    Some(map.buffer_snapshot.len())
2262}
2263
2264fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2265    let point = map.display_point_to_point(display_point, Bias::Left);
2266    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2267        return display_point;
2268    };
2269    let offset = excerpt.buffer().point_to_offset(
2270        excerpt
2271            .buffer()
2272            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2273    );
2274    let buffer_range = excerpt.buffer_range();
2275    if offset >= buffer_range.start && offset <= buffer_range.end {
2276        let point = map
2277            .buffer_snapshot
2278            .offset_to_point(excerpt.map_offset_from_buffer(offset));
2279        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2280    }
2281    let mut last_position = None;
2282    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2283        let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
2284            ..language::ToOffset::to_offset(&range.context.end, buffer);
2285        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2286            let text_anchor = buffer.anchor_after(offset);
2287            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2288            return anchor.to_display_point(map);
2289        } else if offset <= excerpt_range.start {
2290            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2291            return anchor.to_display_point(map);
2292        } else {
2293            last_position = Some(Anchor::in_buffer(
2294                excerpt,
2295                buffer.remote_id(),
2296                range.context.end,
2297            ));
2298        }
2299    }
2300
2301    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2302    last_point.column = point.column;
2303
2304    map.clip_point(
2305        map.point_to_display_point(
2306            map.buffer_snapshot.clip_point(point, Bias::Left),
2307            Bias::Left,
2308        ),
2309        Bias::Left,
2310    )
2311}
2312
2313fn start_of_document(
2314    map: &DisplaySnapshot,
2315    display_point: DisplayPoint,
2316    maybe_times: Option<usize>,
2317) -> DisplayPoint {
2318    if let Some(times) = maybe_times {
2319        return go_to_line(map, display_point, times);
2320    }
2321
2322    let point = map.display_point_to_point(display_point, Bias::Left);
2323    let mut first_point = Point::zero();
2324    first_point.column = point.column;
2325
2326    map.clip_point(
2327        map.point_to_display_point(
2328            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2329            Bias::Left,
2330        ),
2331        Bias::Left,
2332    )
2333}
2334
2335fn end_of_document(
2336    map: &DisplaySnapshot,
2337    display_point: DisplayPoint,
2338    maybe_times: Option<usize>,
2339) -> DisplayPoint {
2340    if let Some(times) = maybe_times {
2341        return go_to_line(map, display_point, times);
2342    };
2343    let point = map.display_point_to_point(display_point, Bias::Left);
2344    let mut last_point = map.buffer_snapshot.max_point();
2345    last_point.column = point.column;
2346
2347    map.clip_point(
2348        map.point_to_display_point(
2349            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2350            Bias::Left,
2351        ),
2352        Bias::Left,
2353    )
2354}
2355
2356fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2357    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2358    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2359
2360    if head > outer.start && head < inner.start {
2361        let mut offset = inner.end.to_offset(map, Bias::Left);
2362        for c in map.buffer_snapshot.chars_at(offset) {
2363            if c == '/' || c == '\n' || c == '>' {
2364                return Some(offset.to_display_point(map));
2365            }
2366            offset += c.len_utf8();
2367        }
2368    } else {
2369        let mut offset = outer.start.to_offset(map, Bias::Left);
2370        for c in map.buffer_snapshot.chars_at(offset) {
2371            offset += c.len_utf8();
2372            if c == '<' || c == '\n' {
2373                return Some(offset.to_display_point(map));
2374            }
2375        }
2376    }
2377
2378    None
2379}
2380
2381fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2382    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2383    let display_point = map.clip_at_line_end(display_point);
2384    let point = display_point.to_point(map);
2385    let offset = point.to_offset(&map.buffer_snapshot);
2386
2387    // Ensure the range is contained by the current line.
2388    let mut line_end = map.next_line_boundary(point).0;
2389    if line_end == point {
2390        line_end = map.max_point().to_point(map);
2391    }
2392
2393    if let Some((opening_range, closing_range)) = map
2394        .buffer_snapshot
2395        .innermost_enclosing_bracket_ranges(offset..offset, None)
2396    {
2397        if opening_range.contains(&offset) {
2398            return closing_range.start.to_display_point(map);
2399        } else if closing_range.contains(&offset) {
2400            return opening_range.start.to_display_point(map);
2401        }
2402    }
2403
2404    let line_range = map.prev_line_boundary(point).0..line_end;
2405    let visible_line_range =
2406        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2407    let ranges = map.buffer_snapshot.bracket_ranges(visible_line_range);
2408    if let Some(ranges) = ranges {
2409        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2410            ..line_range.end.to_offset(&map.buffer_snapshot);
2411        let mut closest_pair_destination = None;
2412        let mut closest_distance = usize::MAX;
2413
2414        for (open_range, close_range) in ranges {
2415            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2416                if offset > open_range.start && offset < close_range.start {
2417                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2418                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2419                        return display_point;
2420                    }
2421                    if let Some(tag) = matching_tag(map, display_point) {
2422                        return tag;
2423                    }
2424                } else if close_range.contains(&offset) {
2425                    return open_range.start.to_display_point(map);
2426                } else if open_range.contains(&offset) {
2427                    return (close_range.end - 1).to_display_point(map);
2428                }
2429            }
2430
2431            if (open_range.contains(&offset) || open_range.start >= offset)
2432                && line_range.contains(&open_range.start)
2433            {
2434                let distance = open_range.start.saturating_sub(offset);
2435                if distance < closest_distance {
2436                    closest_pair_destination = Some(close_range.start);
2437                    closest_distance = distance;
2438                    continue;
2439                }
2440            }
2441
2442            if (close_range.contains(&offset) || close_range.start >= offset)
2443                && line_range.contains(&close_range.start)
2444            {
2445                let distance = close_range.start.saturating_sub(offset);
2446                if distance < closest_distance {
2447                    closest_pair_destination = Some(open_range.start);
2448                    closest_distance = distance;
2449                    continue;
2450                }
2451            }
2452
2453            continue;
2454        }
2455
2456        closest_pair_destination
2457            .map(|destination| destination.to_display_point(map))
2458            .unwrap_or(display_point)
2459    } else {
2460        display_point
2461    }
2462}
2463
2464// Go to {count} percentage in the file, on the first
2465// non-blank in the line linewise.  To compute the new
2466// line number this formula is used:
2467// ({count} * number-of-lines + 99) / 100
2468//
2469// https://neovim.io/doc/user/motion.html#N%25
2470fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2471    let total_lines = map.buffer_snapshot.max_point().row + 1;
2472    let target_line = (count * total_lines as usize).div_ceil(100);
2473    let target_point = DisplayPoint::new(
2474        DisplayRow(target_line.saturating_sub(1) as u32),
2475        point.column(),
2476    );
2477    map.clip_point(target_point, Bias::Left)
2478}
2479
2480fn unmatched_forward(
2481    map: &DisplaySnapshot,
2482    mut display_point: DisplayPoint,
2483    char: char,
2484    times: usize,
2485) -> DisplayPoint {
2486    for _ in 0..times {
2487        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2488        let point = display_point.to_point(map);
2489        let offset = point.to_offset(&map.buffer_snapshot);
2490
2491        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2492        let Some(ranges) = ranges else { break };
2493        let mut closest_closing_destination = None;
2494        let mut closest_distance = usize::MAX;
2495
2496        for (_, close_range) in ranges {
2497            if close_range.start > offset {
2498                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2499                if Some(char) == chars.next() {
2500                    let distance = close_range.start - offset;
2501                    if distance < closest_distance {
2502                        closest_closing_destination = Some(close_range.start);
2503                        closest_distance = distance;
2504                        continue;
2505                    }
2506                }
2507            }
2508        }
2509
2510        let new_point = closest_closing_destination
2511            .map(|destination| destination.to_display_point(map))
2512            .unwrap_or(display_point);
2513        if new_point == display_point {
2514            break;
2515        }
2516        display_point = new_point;
2517    }
2518    display_point
2519}
2520
2521fn unmatched_backward(
2522    map: &DisplaySnapshot,
2523    mut display_point: DisplayPoint,
2524    char: char,
2525    times: usize,
2526) -> DisplayPoint {
2527    for _ in 0..times {
2528        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2529        let point = display_point.to_point(map);
2530        let offset = point.to_offset(&map.buffer_snapshot);
2531
2532        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2533        let Some(ranges) = ranges else {
2534            break;
2535        };
2536
2537        let mut closest_starting_destination = None;
2538        let mut closest_distance = usize::MAX;
2539
2540        for (start_range, _) in ranges {
2541            if start_range.start < offset {
2542                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2543                if Some(char) == chars.next() {
2544                    let distance = offset - start_range.start;
2545                    if distance < closest_distance {
2546                        closest_starting_destination = Some(start_range.start);
2547                        closest_distance = distance;
2548                        continue;
2549                    }
2550                }
2551            }
2552        }
2553
2554        let new_point = closest_starting_destination
2555            .map(|destination| destination.to_display_point(map))
2556            .unwrap_or(display_point);
2557        if new_point == display_point {
2558            break;
2559        } else {
2560            display_point = new_point;
2561        }
2562    }
2563    display_point
2564}
2565
2566fn find_forward(
2567    map: &DisplaySnapshot,
2568    from: DisplayPoint,
2569    before: bool,
2570    target: char,
2571    times: usize,
2572    mode: FindRange,
2573    smartcase: bool,
2574) -> Option<DisplayPoint> {
2575    let mut to = from;
2576    let mut found = false;
2577
2578    for _ in 0..times {
2579        found = false;
2580        let new_to = find_boundary(map, to, mode, |_, right| {
2581            found = is_character_match(target, right, smartcase);
2582            found
2583        });
2584        if to == new_to {
2585            break;
2586        }
2587        to = new_to;
2588    }
2589
2590    if found {
2591        if before && to.column() > 0 {
2592            *to.column_mut() -= 1;
2593            Some(map.clip_point(to, Bias::Left))
2594        } else if before && to.row().0 > 0 {
2595            *to.row_mut() -= 1;
2596            *to.column_mut() = map.line(to.row()).len() as u32;
2597            Some(map.clip_point(to, Bias::Left))
2598        } else {
2599            Some(to)
2600        }
2601    } else {
2602        None
2603    }
2604}
2605
2606fn find_backward(
2607    map: &DisplaySnapshot,
2608    from: DisplayPoint,
2609    after: bool,
2610    target: char,
2611    times: usize,
2612    mode: FindRange,
2613    smartcase: bool,
2614) -> DisplayPoint {
2615    let mut to = from;
2616
2617    for _ in 0..times {
2618        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2619            is_character_match(target, right, smartcase)
2620        });
2621        if to == new_to {
2622            break;
2623        }
2624        to = new_to;
2625    }
2626
2627    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2628    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2629        if after {
2630            *to.column_mut() += 1;
2631            map.clip_point(to, Bias::Right)
2632        } else {
2633            to
2634        }
2635    } else {
2636        from
2637    }
2638}
2639
2640/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2641pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2642    if smartcase {
2643        if target.is_uppercase() {
2644            target == other
2645        } else {
2646            target == other.to_ascii_lowercase()
2647        }
2648    } else {
2649        target == other
2650    }
2651}
2652
2653fn sneak(
2654    map: &DisplaySnapshot,
2655    from: DisplayPoint,
2656    first_target: char,
2657    second_target: char,
2658    times: usize,
2659    smartcase: bool,
2660) -> Option<DisplayPoint> {
2661    let mut to = from;
2662    let mut found = false;
2663
2664    for _ in 0..times {
2665        found = false;
2666        let new_to = find_boundary(
2667            map,
2668            movement::right(map, to),
2669            FindRange::MultiLine,
2670            |left, right| {
2671                found = is_character_match(first_target, left, smartcase)
2672                    && is_character_match(second_target, right, smartcase);
2673                found
2674            },
2675        );
2676        if to == new_to {
2677            break;
2678        }
2679        to = new_to;
2680    }
2681
2682    if found {
2683        Some(movement::left(map, to))
2684    } else {
2685        None
2686    }
2687}
2688
2689fn sneak_backward(
2690    map: &DisplaySnapshot,
2691    from: DisplayPoint,
2692    first_target: char,
2693    second_target: char,
2694    times: usize,
2695    smartcase: bool,
2696) -> Option<DisplayPoint> {
2697    let mut to = from;
2698    let mut found = false;
2699
2700    for _ in 0..times {
2701        found = false;
2702        let new_to =
2703            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2704                found = is_character_match(first_target, left, smartcase)
2705                    && is_character_match(second_target, right, smartcase);
2706                found
2707            });
2708        if to == new_to {
2709            break;
2710        }
2711        to = new_to;
2712    }
2713
2714    if found {
2715        Some(movement::left(map, to))
2716    } else {
2717        None
2718    }
2719}
2720
2721fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2722    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2723    first_non_whitespace(map, false, correct_line)
2724}
2725
2726fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2727    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2728    first_non_whitespace(map, false, correct_line)
2729}
2730
2731fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2732    let correct_line = start_of_relative_buffer_row(map, point, 0);
2733    right(map, correct_line, times.saturating_sub(1))
2734}
2735
2736pub(crate) fn next_line_end(
2737    map: &DisplaySnapshot,
2738    mut point: DisplayPoint,
2739    times: usize,
2740) -> DisplayPoint {
2741    if times > 1 {
2742        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2743    }
2744    end_of_line(map, false, point, 1)
2745}
2746
2747fn window_top(
2748    map: &DisplaySnapshot,
2749    point: DisplayPoint,
2750    text_layout_details: &TextLayoutDetails,
2751    mut times: usize,
2752) -> (DisplayPoint, SelectionGoal) {
2753    let first_visible_line = text_layout_details
2754        .scroll_anchor
2755        .anchor
2756        .to_display_point(map);
2757
2758    if first_visible_line.row() != DisplayRow(0)
2759        && text_layout_details.vertical_scroll_margin as usize > times
2760    {
2761        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2762    }
2763
2764    if let Some(visible_rows) = text_layout_details.visible_rows {
2765        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2766        let new_row = (first_visible_line.row().0 + (times as u32))
2767            .min(bottom_row)
2768            .min(map.max_point().row().0);
2769        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2770
2771        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2772        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2773    } else {
2774        let new_row =
2775            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2776        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2777
2778        let new_point = DisplayPoint::new(new_row, new_col);
2779        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2780    }
2781}
2782
2783fn window_middle(
2784    map: &DisplaySnapshot,
2785    point: DisplayPoint,
2786    text_layout_details: &TextLayoutDetails,
2787) -> (DisplayPoint, SelectionGoal) {
2788    if let Some(visible_rows) = text_layout_details.visible_rows {
2789        let first_visible_line = text_layout_details
2790            .scroll_anchor
2791            .anchor
2792            .to_display_point(map);
2793
2794        let max_visible_rows =
2795            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2796
2797        let new_row =
2798            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2799        let new_row = DisplayRow(new_row);
2800        let new_col = point.column().min(map.line_len(new_row));
2801        let new_point = DisplayPoint::new(new_row, new_col);
2802        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2803    } else {
2804        (point, SelectionGoal::None)
2805    }
2806}
2807
2808fn window_bottom(
2809    map: &DisplaySnapshot,
2810    point: DisplayPoint,
2811    text_layout_details: &TextLayoutDetails,
2812    mut times: usize,
2813) -> (DisplayPoint, SelectionGoal) {
2814    if let Some(visible_rows) = text_layout_details.visible_rows {
2815        let first_visible_line = text_layout_details
2816            .scroll_anchor
2817            .anchor
2818            .to_display_point(map);
2819        let bottom_row = first_visible_line.row().0
2820            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2821        if bottom_row < map.max_point().row().0
2822            && text_layout_details.vertical_scroll_margin as usize > times
2823        {
2824            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2825        }
2826        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2827        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2828        {
2829            first_visible_line.row()
2830        } else {
2831            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2832        };
2833        let new_col = point.column().min(map.line_len(new_row));
2834        let new_point = DisplayPoint::new(new_row, new_col);
2835        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2836    } else {
2837        (point, SelectionGoal::None)
2838    }
2839}
2840
2841fn method_motion(
2842    map: &DisplaySnapshot,
2843    mut display_point: DisplayPoint,
2844    times: usize,
2845    direction: Direction,
2846    is_start: bool,
2847) -> DisplayPoint {
2848    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2849        return display_point;
2850    };
2851
2852    for _ in 0..times {
2853        let point = map.display_point_to_point(display_point, Bias::Left);
2854        let offset = point.to_offset(&map.buffer_snapshot);
2855        let range = if direction == Direction::Prev {
2856            0..offset
2857        } else {
2858            offset..buffer.len()
2859        };
2860
2861        let possibilities = buffer
2862            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2863            .filter_map(|(range, object)| {
2864                if !matches!(object, language::TextObject::AroundFunction) {
2865                    return None;
2866                }
2867
2868                let relevant = if is_start { range.start } else { range.end };
2869                if direction == Direction::Prev && relevant < offset {
2870                    Some(relevant)
2871                } else if direction == Direction::Next && relevant > offset + 1 {
2872                    Some(relevant)
2873                } else {
2874                    None
2875                }
2876            });
2877
2878        let dest = if direction == Direction::Prev {
2879            possibilities.max().unwrap_or(offset)
2880        } else {
2881            possibilities.min().unwrap_or(offset)
2882        };
2883        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2884        if new_point == display_point {
2885            break;
2886        }
2887        display_point = new_point;
2888    }
2889    display_point
2890}
2891
2892fn comment_motion(
2893    map: &DisplaySnapshot,
2894    mut display_point: DisplayPoint,
2895    times: usize,
2896    direction: Direction,
2897) -> DisplayPoint {
2898    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2899        return display_point;
2900    };
2901
2902    for _ in 0..times {
2903        let point = map.display_point_to_point(display_point, Bias::Left);
2904        let offset = point.to_offset(&map.buffer_snapshot);
2905        let range = if direction == Direction::Prev {
2906            0..offset
2907        } else {
2908            offset..buffer.len()
2909        };
2910
2911        let possibilities = buffer
2912            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2913            .filter_map(|(range, object)| {
2914                if !matches!(object, language::TextObject::AroundComment) {
2915                    return None;
2916                }
2917
2918                let relevant = if direction == Direction::Prev {
2919                    range.start
2920                } else {
2921                    range.end
2922                };
2923                if direction == Direction::Prev && relevant < offset {
2924                    Some(relevant)
2925                } else if direction == Direction::Next && relevant > offset + 1 {
2926                    Some(relevant)
2927                } else {
2928                    None
2929                }
2930            });
2931
2932        let dest = if direction == Direction::Prev {
2933            possibilities.max().unwrap_or(offset)
2934        } else {
2935            possibilities.min().unwrap_or(offset)
2936        };
2937        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2938        if new_point == display_point {
2939            break;
2940        }
2941        display_point = new_point;
2942    }
2943
2944    display_point
2945}
2946
2947fn section_motion(
2948    map: &DisplaySnapshot,
2949    mut display_point: DisplayPoint,
2950    times: usize,
2951    direction: Direction,
2952    is_start: bool,
2953) -> DisplayPoint {
2954    if map.buffer_snapshot.as_singleton().is_some() {
2955        for _ in 0..times {
2956            let offset = map
2957                .display_point_to_point(display_point, Bias::Left)
2958                .to_offset(&map.buffer_snapshot);
2959            let range = if direction == Direction::Prev {
2960                0..offset
2961            } else {
2962                offset..map.buffer_snapshot.len()
2963            };
2964
2965            // we set a max start depth here because we want a section to only be "top level"
2966            // similar to vim's default of '{' in the first column.
2967            // (and without it, ]] at the start of editor.rs is -very- slow)
2968            let mut possibilities = map
2969                .buffer_snapshot
2970                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2971                .filter(|(_, object)| {
2972                    matches!(
2973                        object,
2974                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2975                    )
2976                })
2977                .collect::<Vec<_>>();
2978            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2979            let mut prev_end = None;
2980            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2981                if t == language::TextObject::AroundFunction
2982                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2983                {
2984                    return None;
2985                }
2986                prev_end = Some(range.end);
2987
2988                let relevant = if is_start { range.start } else { range.end };
2989                if direction == Direction::Prev && relevant < offset {
2990                    Some(relevant)
2991                } else if direction == Direction::Next && relevant > offset + 1 {
2992                    Some(relevant)
2993                } else {
2994                    None
2995                }
2996            });
2997
2998            let offset = if direction == Direction::Prev {
2999                possibilities.max().unwrap_or(0)
3000            } else {
3001                possibilities.min().unwrap_or(map.buffer_snapshot.len())
3002            };
3003
3004            let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3005            if new_point == display_point {
3006                break;
3007            }
3008            display_point = new_point;
3009        }
3010        return display_point;
3011    };
3012
3013    for _ in 0..times {
3014        let next_point = if is_start {
3015            movement::start_of_excerpt(map, display_point, direction)
3016        } else {
3017            movement::end_of_excerpt(map, display_point, direction)
3018        };
3019        if next_point == display_point {
3020            break;
3021        }
3022        display_point = next_point;
3023    }
3024
3025    display_point
3026}
3027
3028fn matches_indent_type(
3029    target_indent: &text::LineIndent,
3030    current_indent: &text::LineIndent,
3031    indent_type: IndentType,
3032) -> bool {
3033    match indent_type {
3034        IndentType::Lesser => {
3035            target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3036        }
3037        IndentType::Greater => {
3038            target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3039        }
3040        IndentType::Same => {
3041            target_indent.spaces == current_indent.spaces
3042                && target_indent.tabs == current_indent.tabs
3043        }
3044    }
3045}
3046
3047fn indent_motion(
3048    map: &DisplaySnapshot,
3049    mut display_point: DisplayPoint,
3050    times: usize,
3051    direction: Direction,
3052    indent_type: IndentType,
3053) -> DisplayPoint {
3054    let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3055    let current_row = MultiBufferRow(buffer_point.row);
3056    let current_indent = map.line_indent_for_buffer_row(current_row);
3057    if current_indent.is_line_empty() {
3058        return display_point;
3059    }
3060    let max_row = map.max_point().to_point(map).row;
3061
3062    for _ in 0..times {
3063        let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3064
3065        let target_row = match direction {
3066            Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3067                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3068                !indent.is_line_empty()
3069                    && matches_indent_type(&indent, &current_indent, indent_type)
3070            }),
3071            Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3072                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3073                !indent.is_line_empty()
3074                    && matches_indent_type(&indent, &current_indent, indent_type)
3075            }),
3076        }
3077        .unwrap_or(current_buffer_row);
3078
3079        let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3080        let new_point = first_non_whitespace(map, false, new_point);
3081        if new_point == display_point {
3082            break;
3083        }
3084        display_point = new_point;
3085    }
3086    display_point
3087}
3088
3089#[cfg(test)]
3090mod test {
3091
3092    use crate::{
3093        state::Mode,
3094        test::{NeovimBackedTestContext, VimTestContext},
3095    };
3096    use editor::display_map::Inlay;
3097    use indoc::indoc;
3098    use language::Point;
3099    use multi_buffer::MultiBufferRow;
3100
3101    #[gpui::test]
3102    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3103        let mut cx = NeovimBackedTestContext::new(cx).await;
3104
3105        let initial_state = indoc! {r"ˇabc
3106            def
3107
3108            paragraph
3109            the second
3110
3111
3112
3113            third and
3114            final"};
3115
3116        // goes down once
3117        cx.set_shared_state(initial_state).await;
3118        cx.simulate_shared_keystrokes("}").await;
3119        cx.shared_state().await.assert_eq(indoc! {r"abc
3120            def
3121            ˇ
3122            paragraph
3123            the second
3124
3125
3126
3127            third and
3128            final"});
3129
3130        // goes up once
3131        cx.simulate_shared_keystrokes("{").await;
3132        cx.shared_state().await.assert_eq(initial_state);
3133
3134        // goes down twice
3135        cx.simulate_shared_keystrokes("2 }").await;
3136        cx.shared_state().await.assert_eq(indoc! {r"abc
3137            def
3138
3139            paragraph
3140            the second
3141            ˇ
3142
3143
3144            third and
3145            final"});
3146
3147        // goes down over multiple blanks
3148        cx.simulate_shared_keystrokes("}").await;
3149        cx.shared_state().await.assert_eq(indoc! {r"abc
3150                def
3151
3152                paragraph
3153                the second
3154
3155
3156
3157                third and
3158                finaˇl"});
3159
3160        // goes up twice
3161        cx.simulate_shared_keystrokes("2 {").await;
3162        cx.shared_state().await.assert_eq(indoc! {r"abc
3163                def
3164                ˇ
3165                paragraph
3166                the second
3167
3168
3169
3170                third and
3171                final"});
3172    }
3173
3174    #[gpui::test]
3175    async fn test_matching(cx: &mut gpui::TestAppContext) {
3176        let mut cx = NeovimBackedTestContext::new(cx).await;
3177
3178        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3179                do(something(with<Types>.and_arrays[0, 2]))
3180            }"})
3181            .await;
3182        cx.simulate_shared_keystrokes("%").await;
3183        cx.shared_state()
3184            .await
3185            .assert_eq(indoc! {r"func (a stringˇ) {
3186                do(something(with<Types>.and_arrays[0, 2]))
3187            }"});
3188
3189        // test it works on the last character of the line
3190        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3191            do(something(with<Types>.and_arrays[0, 2]))
3192            }"})
3193            .await;
3194        cx.simulate_shared_keystrokes("%").await;
3195        cx.shared_state()
3196            .await
3197            .assert_eq(indoc! {r"func (a string) {
3198            do(something(with<Types>.and_arrays[0, 2]))
3199            ˇ}"});
3200
3201        // test it works on immediate nesting
3202        cx.set_shared_state("ˇ{()}").await;
3203        cx.simulate_shared_keystrokes("%").await;
3204        cx.shared_state().await.assert_eq("{()ˇ}");
3205        cx.simulate_shared_keystrokes("%").await;
3206        cx.shared_state().await.assert_eq("ˇ{()}");
3207
3208        // test it works on immediate nesting inside braces
3209        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3210        cx.simulate_shared_keystrokes("%").await;
3211        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3212
3213        // test it jumps to the next paren on a line
3214        cx.set_shared_state("func ˇboop() {\n}").await;
3215        cx.simulate_shared_keystrokes("%").await;
3216        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3217    }
3218
3219    #[gpui::test]
3220    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3221        let mut cx = NeovimBackedTestContext::new(cx).await;
3222
3223        // test it works with curly braces
3224        cx.set_shared_state(indoc! {r"func (a string) {
3225                do(something(with<Types>.anˇd_arrays[0, 2]))
3226            }"})
3227            .await;
3228        cx.simulate_shared_keystrokes("] }").await;
3229        cx.shared_state()
3230            .await
3231            .assert_eq(indoc! {r"func (a string) {
3232                do(something(with<Types>.and_arrays[0, 2]))
3233            ˇ}"});
3234
3235        // test it works with brackets
3236        cx.set_shared_state(indoc! {r"func (a string) {
3237                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3238            }"})
3239            .await;
3240        cx.simulate_shared_keystrokes("] )").await;
3241        cx.shared_state()
3242            .await
3243            .assert_eq(indoc! {r"func (a string) {
3244                do(something(with<Types>.and_arrays[0, 2])ˇ)
3245            }"});
3246
3247        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3248            .await;
3249        cx.simulate_shared_keystrokes("] )").await;
3250        cx.shared_state()
3251            .await
3252            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3253
3254        // test it works on immediate nesting
3255        cx.set_shared_state("{ˇ {}{}}").await;
3256        cx.simulate_shared_keystrokes("] }").await;
3257        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3258        cx.set_shared_state("(ˇ ()())").await;
3259        cx.simulate_shared_keystrokes("] )").await;
3260        cx.shared_state().await.assert_eq("( ()()ˇ)");
3261
3262        // test it works on immediate nesting inside braces
3263        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3264        cx.simulate_shared_keystrokes("] }").await;
3265        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3266        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3267        cx.simulate_shared_keystrokes("] )").await;
3268        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3269    }
3270
3271    #[gpui::test]
3272    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3273        let mut cx = NeovimBackedTestContext::new(cx).await;
3274
3275        // test it works with curly braces
3276        cx.set_shared_state(indoc! {r"func (a string) {
3277                do(something(with<Types>.anˇd_arrays[0, 2]))
3278            }"})
3279            .await;
3280        cx.simulate_shared_keystrokes("[ {").await;
3281        cx.shared_state()
3282            .await
3283            .assert_eq(indoc! {r"func (a string) ˇ{
3284                do(something(with<Types>.and_arrays[0, 2]))
3285            }"});
3286
3287        // test it works with brackets
3288        cx.set_shared_state(indoc! {r"func (a string) {
3289                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3290            }"})
3291            .await;
3292        cx.simulate_shared_keystrokes("[ (").await;
3293        cx.shared_state()
3294            .await
3295            .assert_eq(indoc! {r"func (a string) {
3296                doˇ(something(with<Types>.and_arrays[0, 2]))
3297            }"});
3298
3299        // test it works on immediate nesting
3300        cx.set_shared_state("{{}{} ˇ }").await;
3301        cx.simulate_shared_keystrokes("[ {").await;
3302        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3303        cx.set_shared_state("(()() ˇ )").await;
3304        cx.simulate_shared_keystrokes("[ (").await;
3305        cx.shared_state().await.assert_eq("ˇ(()()  )");
3306
3307        // test it works on immediate nesting inside braces
3308        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3309        cx.simulate_shared_keystrokes("[ {").await;
3310        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3311        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3312        cx.simulate_shared_keystrokes("[ (").await;
3313        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3314    }
3315
3316    #[gpui::test]
3317    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3318        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3319
3320        cx.neovim.exec("set filetype=html").await;
3321
3322        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3323        cx.simulate_shared_keystrokes("%").await;
3324        cx.shared_state()
3325            .await
3326            .assert_eq(indoc! {r"<body><ˇ/body>"});
3327        cx.simulate_shared_keystrokes("%").await;
3328
3329        // test jumping backwards
3330        cx.shared_state()
3331            .await
3332            .assert_eq(indoc! {r"<ˇbody></body>"});
3333
3334        // test self-closing tags
3335        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3336        cx.simulate_shared_keystrokes("%").await;
3337        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3338
3339        // test tag with attributes
3340        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3341            </div>
3342            "})
3343            .await;
3344        cx.simulate_shared_keystrokes("%").await;
3345        cx.shared_state()
3346            .await
3347            .assert_eq(indoc! {r"<div class='test' id='main'>
3348            <ˇ/div>
3349            "});
3350
3351        // test multi-line self-closing tag
3352        cx.set_shared_state(indoc! {r#"<a>
3353            <br
3354                test = "test"
3355            /ˇ>
3356        </a>"#})
3357            .await;
3358        cx.simulate_shared_keystrokes("%").await;
3359        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3360            ˇ<br
3361                test = "test"
3362            />
3363        </a>"#});
3364    }
3365
3366    #[gpui::test]
3367    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3368        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3369
3370        // test brackets within tags
3371        cx.set_shared_state(indoc! {r"function f() {
3372            return (
3373                <div rules={ˇ[{ a: 1 }]}>
3374                    <h1>test</h1>
3375                </div>
3376            );
3377        }"})
3378            .await;
3379        cx.simulate_shared_keystrokes("%").await;
3380        cx.shared_state().await.assert_eq(indoc! {r"function f() {
3381            return (
3382                <div rules={[{ a: 1 }ˇ]}>
3383                    <h1>test</h1>
3384                </div>
3385            );
3386        }"});
3387    }
3388
3389    #[gpui::test]
3390    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3391        let mut cx = NeovimBackedTestContext::new(cx).await;
3392
3393        // f and F
3394        cx.set_shared_state("ˇone two three four").await;
3395        cx.simulate_shared_keystrokes("f o").await;
3396        cx.shared_state().await.assert_eq("one twˇo three four");
3397        cx.simulate_shared_keystrokes(",").await;
3398        cx.shared_state().await.assert_eq("ˇone two three four");
3399        cx.simulate_shared_keystrokes("2 ;").await;
3400        cx.shared_state().await.assert_eq("one two three fˇour");
3401        cx.simulate_shared_keystrokes("shift-f e").await;
3402        cx.shared_state().await.assert_eq("one two threˇe four");
3403        cx.simulate_shared_keystrokes("2 ;").await;
3404        cx.shared_state().await.assert_eq("onˇe two three four");
3405        cx.simulate_shared_keystrokes(",").await;
3406        cx.shared_state().await.assert_eq("one two thrˇee four");
3407
3408        // t and T
3409        cx.set_shared_state("ˇone two three four").await;
3410        cx.simulate_shared_keystrokes("t o").await;
3411        cx.shared_state().await.assert_eq("one tˇwo three four");
3412        cx.simulate_shared_keystrokes(",").await;
3413        cx.shared_state().await.assert_eq("oˇne two three four");
3414        cx.simulate_shared_keystrokes("2 ;").await;
3415        cx.shared_state().await.assert_eq("one two three ˇfour");
3416        cx.simulate_shared_keystrokes("shift-t e").await;
3417        cx.shared_state().await.assert_eq("one two threeˇ four");
3418        cx.simulate_shared_keystrokes("3 ;").await;
3419        cx.shared_state().await.assert_eq("oneˇ two three four");
3420        cx.simulate_shared_keystrokes(",").await;
3421        cx.shared_state().await.assert_eq("one two thˇree four");
3422    }
3423
3424    #[gpui::test]
3425    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3426        let mut cx = NeovimBackedTestContext::new(cx).await;
3427        let initial_state = indoc! {r"something(ˇfoo)"};
3428        cx.set_shared_state(initial_state).await;
3429        cx.simulate_shared_keystrokes("}").await;
3430        cx.shared_state().await.assert_eq("something(fooˇ)");
3431    }
3432
3433    #[gpui::test]
3434    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3435        let mut cx = NeovimBackedTestContext::new(cx).await;
3436        cx.set_shared_state("ˇone\n  two\nthree").await;
3437        cx.simulate_shared_keystrokes("enter").await;
3438        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3439    }
3440
3441    #[gpui::test]
3442    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3443        let mut cx = NeovimBackedTestContext::new(cx).await;
3444        cx.set_shared_state("ˇ one\n two \nthree").await;
3445        cx.simulate_shared_keystrokes("g _").await;
3446        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3447
3448        cx.set_shared_state("ˇ one \n two \nthree").await;
3449        cx.simulate_shared_keystrokes("g _").await;
3450        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3451        cx.simulate_shared_keystrokes("2 g _").await;
3452        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3453    }
3454
3455    #[gpui::test]
3456    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3457        let mut cx = NeovimBackedTestContext::new(cx).await;
3458        let initial_state = indoc! {r"abc
3459          def
3460          paragraph
3461          the second
3462          third ˇand
3463          final"};
3464
3465        cx.set_shared_state(initial_state).await;
3466        cx.simulate_shared_keystrokes("shift-h").await;
3467        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3468          def
3469          paragraph
3470          the second
3471          third and
3472          final"});
3473
3474        // clip point
3475        cx.set_shared_state(indoc! {r"
3476          1 2 3
3477          4 5 6
3478          7 8 ˇ9
3479          "})
3480            .await;
3481        cx.simulate_shared_keystrokes("shift-h").await;
3482        cx.shared_state().await.assert_eq(indoc! {"
3483          1 2 ˇ3
3484          4 5 6
3485          7 8 9
3486          "});
3487
3488        cx.set_shared_state(indoc! {r"
3489          1 2 3
3490          4 5 6
3491          ˇ7 8 9
3492          "})
3493            .await;
3494        cx.simulate_shared_keystrokes("shift-h").await;
3495        cx.shared_state().await.assert_eq(indoc! {"
3496          ˇ1 2 3
3497          4 5 6
3498          7 8 9
3499          "});
3500
3501        cx.set_shared_state(indoc! {r"
3502          1 2 3
3503          4 5 ˇ6
3504          7 8 9"})
3505            .await;
3506        cx.simulate_shared_keystrokes("9 shift-h").await;
3507        cx.shared_state().await.assert_eq(indoc! {"
3508          1 2 3
3509          4 5 6
3510          7 8 ˇ9"});
3511    }
3512
3513    #[gpui::test]
3514    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3515        let mut cx = NeovimBackedTestContext::new(cx).await;
3516        let initial_state = indoc! {r"abˇc
3517          def
3518          paragraph
3519          the second
3520          third and
3521          final"};
3522
3523        cx.set_shared_state(initial_state).await;
3524        cx.simulate_shared_keystrokes("shift-m").await;
3525        cx.shared_state().await.assert_eq(indoc! {r"abc
3526          def
3527          paˇragraph
3528          the second
3529          third and
3530          final"});
3531
3532        cx.set_shared_state(indoc! {r"
3533          1 2 3
3534          4 5 6
3535          7 8 ˇ9
3536          "})
3537            .await;
3538        cx.simulate_shared_keystrokes("shift-m").await;
3539        cx.shared_state().await.assert_eq(indoc! {"
3540          1 2 3
3541          4 5 ˇ6
3542          7 8 9
3543          "});
3544        cx.set_shared_state(indoc! {r"
3545          1 2 3
3546          4 5 6
3547          ˇ7 8 9
3548          "})
3549            .await;
3550        cx.simulate_shared_keystrokes("shift-m").await;
3551        cx.shared_state().await.assert_eq(indoc! {"
3552          1 2 3
3553          ˇ4 5 6
3554          7 8 9
3555          "});
3556        cx.set_shared_state(indoc! {r"
3557          ˇ1 2 3
3558          4 5 6
3559          7 8 9
3560          "})
3561            .await;
3562        cx.simulate_shared_keystrokes("shift-m").await;
3563        cx.shared_state().await.assert_eq(indoc! {"
3564          1 2 3
3565          ˇ4 5 6
3566          7 8 9
3567          "});
3568        cx.set_shared_state(indoc! {r"
3569          1 2 3
3570          ˇ4 5 6
3571          7 8 9
3572          "})
3573            .await;
3574        cx.simulate_shared_keystrokes("shift-m").await;
3575        cx.shared_state().await.assert_eq(indoc! {"
3576          1 2 3
3577          ˇ4 5 6
3578          7 8 9
3579          "});
3580        cx.set_shared_state(indoc! {r"
3581          1 2 3
3582          4 5 ˇ6
3583          7 8 9
3584          "})
3585            .await;
3586        cx.simulate_shared_keystrokes("shift-m").await;
3587        cx.shared_state().await.assert_eq(indoc! {"
3588          1 2 3
3589          4 5 ˇ6
3590          7 8 9
3591          "});
3592    }
3593
3594    #[gpui::test]
3595    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3596        let mut cx = NeovimBackedTestContext::new(cx).await;
3597        let initial_state = indoc! {r"abc
3598          deˇf
3599          paragraph
3600          the second
3601          third and
3602          final"};
3603
3604        cx.set_shared_state(initial_state).await;
3605        cx.simulate_shared_keystrokes("shift-l").await;
3606        cx.shared_state().await.assert_eq(indoc! {r"abc
3607          def
3608          paragraph
3609          the second
3610          third and
3611          fiˇnal"});
3612
3613        cx.set_shared_state(indoc! {r"
3614          1 2 3
3615          4 5 ˇ6
3616          7 8 9
3617          "})
3618            .await;
3619        cx.simulate_shared_keystrokes("shift-l").await;
3620        cx.shared_state().await.assert_eq(indoc! {"
3621          1 2 3
3622          4 5 6
3623          7 8 9
3624          ˇ"});
3625
3626        cx.set_shared_state(indoc! {r"
3627          1 2 3
3628          ˇ4 5 6
3629          7 8 9
3630          "})
3631            .await;
3632        cx.simulate_shared_keystrokes("shift-l").await;
3633        cx.shared_state().await.assert_eq(indoc! {"
3634          1 2 3
3635          4 5 6
3636          7 8 9
3637          ˇ"});
3638
3639        cx.set_shared_state(indoc! {r"
3640          1 2 ˇ3
3641          4 5 6
3642          7 8 9
3643          "})
3644            .await;
3645        cx.simulate_shared_keystrokes("shift-l").await;
3646        cx.shared_state().await.assert_eq(indoc! {"
3647          1 2 3
3648          4 5 6
3649          7 8 9
3650          ˇ"});
3651
3652        cx.set_shared_state(indoc! {r"
3653          ˇ1 2 3
3654          4 5 6
3655          7 8 9
3656          "})
3657            .await;
3658        cx.simulate_shared_keystrokes("shift-l").await;
3659        cx.shared_state().await.assert_eq(indoc! {"
3660          1 2 3
3661          4 5 6
3662          7 8 9
3663          ˇ"});
3664
3665        cx.set_shared_state(indoc! {r"
3666          1 2 3
3667          4 5 ˇ6
3668          7 8 9
3669          "})
3670            .await;
3671        cx.simulate_shared_keystrokes("9 shift-l").await;
3672        cx.shared_state().await.assert_eq(indoc! {"
3673          1 2 ˇ3
3674          4 5 6
3675          7 8 9
3676          "});
3677    }
3678
3679    #[gpui::test]
3680    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3681        let mut cx = NeovimBackedTestContext::new(cx).await;
3682        cx.set_shared_state(indoc! {r"
3683        456 5ˇ67 678
3684        "})
3685            .await;
3686        cx.simulate_shared_keystrokes("g e").await;
3687        cx.shared_state().await.assert_eq(indoc! {"
3688        45ˇ6 567 678
3689        "});
3690
3691        // Test times
3692        cx.set_shared_state(indoc! {r"
3693        123 234 345
3694        456 5ˇ67 678
3695        "})
3696            .await;
3697        cx.simulate_shared_keystrokes("4 g e").await;
3698        cx.shared_state().await.assert_eq(indoc! {"
3699        12ˇ3 234 345
3700        456 567 678
3701        "});
3702
3703        // With punctuation
3704        cx.set_shared_state(indoc! {r"
3705        123 234 345
3706        4;5.6 5ˇ67 678
3707        789 890 901
3708        "})
3709            .await;
3710        cx.simulate_shared_keystrokes("g e").await;
3711        cx.shared_state().await.assert_eq(indoc! {"
3712          123 234 345
3713          4;5.ˇ6 567 678
3714          789 890 901
3715        "});
3716
3717        // With punctuation and count
3718        cx.set_shared_state(indoc! {r"
3719        123 234 345
3720        4;5.6 5ˇ67 678
3721        789 890 901
3722        "})
3723            .await;
3724        cx.simulate_shared_keystrokes("5 g e").await;
3725        cx.shared_state().await.assert_eq(indoc! {"
3726          123 234 345
3727          ˇ4;5.6 567 678
3728          789 890 901
3729        "});
3730
3731        // newlines
3732        cx.set_shared_state(indoc! {r"
3733        123 234 345
3734
3735        78ˇ9 890 901
3736        "})
3737            .await;
3738        cx.simulate_shared_keystrokes("g e").await;
3739        cx.shared_state().await.assert_eq(indoc! {"
3740          123 234 345
3741          ˇ
3742          789 890 901
3743        "});
3744        cx.simulate_shared_keystrokes("g e").await;
3745        cx.shared_state().await.assert_eq(indoc! {"
3746          123 234 34ˇ5
3747
3748          789 890 901
3749        "});
3750
3751        // With punctuation
3752        cx.set_shared_state(indoc! {r"
3753        123 234 345
3754        4;5.ˇ6 567 678
3755        789 890 901
3756        "})
3757            .await;
3758        cx.simulate_shared_keystrokes("g shift-e").await;
3759        cx.shared_state().await.assert_eq(indoc! {"
3760          123 234 34ˇ5
3761          4;5.6 567 678
3762          789 890 901
3763        "});
3764
3765        // With multi byte char
3766        cx.set_shared_state(indoc! {r"
3767        bar ˇó
3768        "})
3769            .await;
3770        cx.simulate_shared_keystrokes("g e").await;
3771        cx.shared_state().await.assert_eq(indoc! {"
3772        baˇr ó
3773        "});
3774    }
3775
3776    #[gpui::test]
3777    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3778        let mut cx = NeovimBackedTestContext::new(cx).await;
3779
3780        cx.set_shared_state(indoc! {"
3781            fn aˇ() {
3782              return
3783            }
3784        "})
3785            .await;
3786        cx.simulate_shared_keystrokes("v $ %").await;
3787        cx.shared_state().await.assert_eq(indoc! {"
3788            fn a«() {
3789              return
3790            }ˇ»
3791        "});
3792    }
3793
3794    #[gpui::test]
3795    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3796        let mut cx = VimTestContext::new(cx, true).await;
3797
3798        cx.set_state(
3799            indoc! {"
3800                struct Foo {
3801                ˇ
3802                }
3803            "},
3804            Mode::Normal,
3805        );
3806
3807        cx.update_editor(|editor, _window, cx| {
3808            let range = editor.selections.newest_anchor().range();
3809            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3810            let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
3811            editor.splice_inlays(&[], vec![inlay], cx);
3812        });
3813
3814        cx.simulate_keystrokes("j");
3815        cx.assert_state(
3816            indoc! {"
3817                struct Foo {
3818
3819                ˇ}
3820            "},
3821            Mode::Normal,
3822        );
3823    }
3824
3825    #[gpui::test]
3826    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3827        let mut cx = VimTestContext::new(cx, true).await;
3828
3829        cx.set_state(
3830            indoc! {"
3831            ˇstruct Foo {
3832
3833            }
3834        "},
3835            Mode::Normal,
3836        );
3837        cx.update_editor(|editor, _window, cx| {
3838            let snapshot = editor.buffer().read(cx).snapshot(cx);
3839            let end_of_line =
3840                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3841            let inlay_text = " hint";
3842            let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
3843            editor.splice_inlays(&[], vec![inlay], cx);
3844        });
3845        cx.simulate_keystrokes("$");
3846        cx.assert_state(
3847            indoc! {"
3848            struct Foo ˇ{
3849
3850            }
3851        "},
3852            Mode::Normal,
3853        );
3854    }
3855
3856    #[gpui::test]
3857    async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
3858        let mut cx = VimTestContext::new(cx, true).await;
3859
3860        // Test the exact scenario from issue #29134
3861        cx.set_state(
3862            indoc! {"
3863                fn main() {
3864                    let this_is_a_long_name = Vec::<u32>::new();
3865                    let new_oneˇ = this_is_a_long_name
3866                        .iter()
3867                        .map(|i| i + 1)
3868                        .map(|i| i * 2)
3869                        .collect::<Vec<_>>();
3870                }
3871            "},
3872            Mode::Normal,
3873        );
3874
3875        // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
3876        cx.update_editor(|editor, _window, cx| {
3877            let snapshot = editor.buffer().read(cx).snapshot(cx);
3878            // The empty line is at line 3 (0-indexed)
3879            let line_start = snapshot.anchor_after(Point::new(3, 0));
3880            let inlay_text = ": Vec<u32>";
3881            let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
3882            editor.splice_inlays(&[], vec![inlay], cx);
3883        });
3884
3885        // Enter visual mode
3886        cx.simulate_keystrokes("v");
3887        cx.assert_state(
3888            indoc! {"
3889                fn main() {
3890                    let this_is_a_long_name = Vec::<u32>::new();
3891                    let new_one« ˇ»= this_is_a_long_name
3892                        .iter()
3893                        .map(|i| i + 1)
3894                        .map(|i| i * 2)
3895                        .collect::<Vec<_>>();
3896                }
3897            "},
3898            Mode::Visual,
3899        );
3900
3901        // Move down - should go to the beginning of line 4, not skip to line 5
3902        cx.simulate_keystrokes("j");
3903        cx.assert_state(
3904            indoc! {"
3905                fn main() {
3906                    let this_is_a_long_name = Vec::<u32>::new();
3907                    let new_one« = this_is_a_long_name
3908                      ˇ»  .iter()
3909                        .map(|i| i + 1)
3910                        .map(|i| i * 2)
3911                        .collect::<Vec<_>>();
3912                }
3913            "},
3914            Mode::Visual,
3915        );
3916
3917        // Test with multiple movements
3918        cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
3919
3920        // Add type hint on the empty line
3921        cx.update_editor(|editor, _window, cx| {
3922            let snapshot = editor.buffer().read(cx).snapshot(cx);
3923            let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
3924            let inlay_text = ": i32";
3925            let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
3926            editor.splice_inlays(&[], vec![inlay], cx);
3927        });
3928
3929        // Enter visual mode and move down twice
3930        cx.simulate_keystrokes("v j j");
3931        cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
3932    }
3933
3934    #[gpui::test]
3935    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3936        let mut cx = NeovimBackedTestContext::new(cx).await;
3937        // Normal mode
3938        cx.set_shared_state(indoc! {"
3939            The ˇquick brown
3940            fox jumps over
3941            the lazy dog
3942            The quick brown
3943            fox jumps over
3944            the lazy dog
3945            The quick brown
3946            fox jumps over
3947            the lazy dog"})
3948            .await;
3949        cx.simulate_shared_keystrokes("2 0 %").await;
3950        cx.shared_state().await.assert_eq(indoc! {"
3951            The quick brown
3952            fox ˇjumps over
3953            the lazy dog
3954            The quick brown
3955            fox jumps over
3956            the lazy dog
3957            The quick brown
3958            fox jumps over
3959            the lazy dog"});
3960
3961        cx.simulate_shared_keystrokes("2 5 %").await;
3962        cx.shared_state().await.assert_eq(indoc! {"
3963            The quick brown
3964            fox jumps over
3965            the ˇlazy dog
3966            The quick brown
3967            fox jumps over
3968            the lazy dog
3969            The quick brown
3970            fox jumps over
3971            the lazy dog"});
3972
3973        cx.simulate_shared_keystrokes("7 5 %").await;
3974        cx.shared_state().await.assert_eq(indoc! {"
3975            The quick brown
3976            fox jumps over
3977            the lazy dog
3978            The quick brown
3979            fox jumps over
3980            the lazy dog
3981            The ˇquick brown
3982            fox jumps over
3983            the lazy dog"});
3984
3985        // Visual mode
3986        cx.set_shared_state(indoc! {"
3987            The ˇquick brown
3988            fox jumps over
3989            the lazy dog
3990            The quick brown
3991            fox jumps over
3992            the lazy dog
3993            The quick brown
3994            fox jumps over
3995            the lazy dog"})
3996            .await;
3997        cx.simulate_shared_keystrokes("v 5 0 %").await;
3998        cx.shared_state().await.assert_eq(indoc! {"
3999            The «quick brown
4000            fox jumps over
4001            the lazy dog
4002            The quick brown
4003            fox jˇ»umps over
4004            the lazy dog
4005            The quick brown
4006            fox jumps over
4007            the lazy dog"});
4008
4009        cx.set_shared_state(indoc! {"
4010            The ˇquick brown
4011            fox jumps over
4012            the lazy dog
4013            The quick brown
4014            fox jumps over
4015            the lazy dog
4016            The quick brown
4017            fox jumps over
4018            the lazy dog"})
4019            .await;
4020        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4021        cx.shared_state().await.assert_eq(indoc! {"
4022            The «quick brown
4023            fox jumps over
4024            the lazy dog
4025            The quick brown
4026            fox jumps over
4027            the lazy dog
4028            The quick brown
4029            fox jumps over
4030            the lˇ»azy dog"});
4031    }
4032
4033    #[gpui::test]
4034    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4035        let mut cx = NeovimBackedTestContext::new(cx).await;
4036
4037        cx.set_shared_state("ˇπππππ").await;
4038        cx.simulate_shared_keystrokes("3 space").await;
4039        cx.shared_state().await.assert_eq("πππˇππ");
4040    }
4041
4042    #[gpui::test]
4043    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4044        let mut cx = NeovimBackedTestContext::new(cx).await;
4045
4046        cx.set_shared_state(indoc! {"
4047            ππππˇπ
4048            πanotherline"})
4049            .await;
4050        cx.simulate_shared_keystrokes("4 space").await;
4051        cx.shared_state().await.assert_eq(indoc! {"
4052            πππππ
4053            πanˇotherline"});
4054    }
4055
4056    #[gpui::test]
4057    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4058        let mut cx = NeovimBackedTestContext::new(cx).await;
4059
4060        cx.set_shared_state(indoc! {"
4061                        ππππ
4062                        πanˇotherline"})
4063            .await;
4064        cx.simulate_shared_keystrokes("4 backspace").await;
4065        cx.shared_state().await.assert_eq(indoc! {"
4066                        πππˇπ
4067                        πanotherline"});
4068    }
4069
4070    #[gpui::test]
4071    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4072        let mut cx = VimTestContext::new(cx, true).await;
4073        cx.set_state(
4074            indoc! {
4075                "func empty(a string) bool {
4076                     ˇif a == \"\" {
4077                         return true
4078                     }
4079                     return false
4080                }"
4081            },
4082            Mode::Normal,
4083        );
4084        cx.simulate_keystrokes("[ -");
4085        cx.assert_state(
4086            indoc! {
4087                "ˇfunc empty(a string) bool {
4088                     if a == \"\" {
4089                         return true
4090                     }
4091                     return false
4092                }"
4093            },
4094            Mode::Normal,
4095        );
4096        cx.simulate_keystrokes("] =");
4097        cx.assert_state(
4098            indoc! {
4099                "func empty(a string) bool {
4100                     if a == \"\" {
4101                         return true
4102                     }
4103                     return false
4104                ˇ}"
4105            },
4106            Mode::Normal,
4107        );
4108        cx.simulate_keystrokes("[ +");
4109        cx.assert_state(
4110            indoc! {
4111                "func empty(a string) bool {
4112                     if a == \"\" {
4113                         return true
4114                     }
4115                     ˇreturn false
4116                }"
4117            },
4118            Mode::Normal,
4119        );
4120        cx.simulate_keystrokes("2 [ =");
4121        cx.assert_state(
4122            indoc! {
4123                "func empty(a string) bool {
4124                     ˇif a == \"\" {
4125                         return true
4126                     }
4127                     return false
4128                }"
4129            },
4130            Mode::Normal,
4131        );
4132        cx.simulate_keystrokes("] +");
4133        cx.assert_state(
4134            indoc! {
4135                "func empty(a string) bool {
4136                     if a == \"\" {
4137                         ˇreturn true
4138                     }
4139                     return false
4140                }"
4141            },
4142            Mode::Normal,
4143        );
4144        cx.simulate_keystrokes("] -");
4145        cx.assert_state(
4146            indoc! {
4147                "func empty(a string) bool {
4148                     if a == \"\" {
4149                         return true
4150                     ˇ}
4151                     return false
4152                }"
4153            },
4154            Mode::Normal,
4155        );
4156    }
4157
4158    #[gpui::test]
4159    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4160        let mut cx = NeovimBackedTestContext::new(cx).await;
4161        cx.set_shared_state("abˇc").await;
4162        cx.simulate_shared_keystrokes("delete").await;
4163        cx.shared_state().await.assert_eq("aˇb");
4164    }
4165
4166    #[gpui::test]
4167    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4168        let mut cx = NeovimBackedTestContext::new(cx).await;
4169
4170        cx.set_shared_state(indoc! {"
4171             ˇthe quick brown fox
4172             jumped over the lazy dog"})
4173            .await;
4174        cx.simulate_shared_keystrokes("d v 0").await;
4175        cx.shared_state().await.assert_eq(indoc! {"
4176             ˇhe quick brown fox
4177             jumped over the lazy dog"});
4178        assert!(!cx.cx.forced_motion());
4179
4180        cx.set_shared_state(indoc! {"
4181            the quick bˇrown fox
4182            jumped over the lazy dog"})
4183            .await;
4184        cx.simulate_shared_keystrokes("d v 0").await;
4185        cx.shared_state().await.assert_eq(indoc! {"
4186            ˇown fox
4187            jumped over the lazy dog"});
4188        assert!(!cx.cx.forced_motion());
4189
4190        cx.set_shared_state(indoc! {"
4191            the quick brown foˇx
4192            jumped over the lazy dog"})
4193            .await;
4194        cx.simulate_shared_keystrokes("d v 0").await;
4195        cx.shared_state().await.assert_eq(indoc! {"
4196            ˇ
4197            jumped over the lazy dog"});
4198        assert!(!cx.cx.forced_motion());
4199    }
4200
4201    #[gpui::test]
4202    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4203        let mut cx = NeovimBackedTestContext::new(cx).await;
4204
4205        cx.set_shared_state(indoc! {"
4206             ˇthe quick brown fox
4207             jumped over the lazy dog"})
4208            .await;
4209        cx.simulate_shared_keystrokes("d v g shift-m").await;
4210        cx.shared_state().await.assert_eq(indoc! {"
4211             ˇbrown fox
4212             jumped over the lazy dog"});
4213        assert!(!cx.cx.forced_motion());
4214
4215        cx.set_shared_state(indoc! {"
4216            the quick bˇrown fox
4217            jumped over the lazy dog"})
4218            .await;
4219        cx.simulate_shared_keystrokes("d v g shift-m").await;
4220        cx.shared_state().await.assert_eq(indoc! {"
4221            the quickˇown fox
4222            jumped over the lazy dog"});
4223        assert!(!cx.cx.forced_motion());
4224
4225        cx.set_shared_state(indoc! {"
4226            the quick brown foˇx
4227            jumped over the lazy dog"})
4228            .await;
4229        cx.simulate_shared_keystrokes("d v g shift-m").await;
4230        cx.shared_state().await.assert_eq(indoc! {"
4231            the quicˇk
4232            jumped over the lazy dog"});
4233        assert!(!cx.cx.forced_motion());
4234
4235        cx.set_shared_state(indoc! {"
4236            ˇthe quick brown fox
4237            jumped over the lazy dog"})
4238            .await;
4239        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4240        cx.shared_state().await.assert_eq(indoc! {"
4241            ˇ fox
4242            jumped over the lazy dog"});
4243        assert!(!cx.cx.forced_motion());
4244
4245        cx.set_shared_state(indoc! {"
4246            ˇthe quick brown fox
4247            jumped over the lazy dog"})
4248            .await;
4249        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4250        cx.shared_state().await.assert_eq(indoc! {"
4251            ˇuick brown fox
4252            jumped over the lazy dog"});
4253        assert!(!cx.cx.forced_motion());
4254    }
4255
4256    #[gpui::test]
4257    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4258        let mut cx = NeovimBackedTestContext::new(cx).await;
4259
4260        cx.set_shared_state(indoc! {"
4261             the quick brown foˇx
4262             jumped over the lazy dog"})
4263            .await;
4264        cx.simulate_shared_keystrokes("d v $").await;
4265        cx.shared_state().await.assert_eq(indoc! {"
4266             the quick brown foˇx
4267             jumped over the lazy dog"});
4268        assert!(!cx.cx.forced_motion());
4269
4270        cx.set_shared_state(indoc! {"
4271             ˇthe quick brown fox
4272             jumped over the lazy dog"})
4273            .await;
4274        cx.simulate_shared_keystrokes("d v $").await;
4275        cx.shared_state().await.assert_eq(indoc! {"
4276             ˇx
4277             jumped over the lazy dog"});
4278        assert!(!cx.cx.forced_motion());
4279    }
4280
4281    #[gpui::test]
4282    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4283        let mut cx = NeovimBackedTestContext::new(cx).await;
4284
4285        cx.set_shared_state(indoc! {"
4286               ˇthe quick brown fox
4287               jumped over the lazy dog"})
4288            .await;
4289        cx.simulate_shared_keystrokes("y v j p").await;
4290        cx.shared_state().await.assert_eq(indoc! {"
4291               the quick brown fox
4292               ˇthe quick brown fox
4293               jumped over the lazy dog"});
4294        assert!(!cx.cx.forced_motion());
4295
4296        cx.set_shared_state(indoc! {"
4297              the quick bˇrown fox
4298              jumped over the lazy dog"})
4299            .await;
4300        cx.simulate_shared_keystrokes("y v j p").await;
4301        cx.shared_state().await.assert_eq(indoc! {"
4302              the quick brˇrown fox
4303              jumped overown fox
4304              jumped over the lazy dog"});
4305        assert!(!cx.cx.forced_motion());
4306
4307        cx.set_shared_state(indoc! {"
4308             the quick brown foˇx
4309             jumped over the lazy dog"})
4310            .await;
4311        cx.simulate_shared_keystrokes("y v j p").await;
4312        cx.shared_state().await.assert_eq(indoc! {"
4313             the quick brown foxˇx
4314             jumped over the la
4315             jumped over the lazy dog"});
4316        assert!(!cx.cx.forced_motion());
4317
4318        cx.set_shared_state(indoc! {"
4319             the quick brown fox
4320             jˇumped over the lazy dog"})
4321            .await;
4322        cx.simulate_shared_keystrokes("y v k p").await;
4323        cx.shared_state().await.assert_eq(indoc! {"
4324            thˇhe quick brown fox
4325            je quick brown fox
4326            jumped over the lazy dog"});
4327        assert!(!cx.cx.forced_motion());
4328    }
4329
4330    #[gpui::test]
4331    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4332        let mut cx = NeovimBackedTestContext::new(cx).await;
4333
4334        cx.set_shared_state(indoc! {"
4335              ˇthe quick brown fox
4336              jumped over the lazy dog"})
4337            .await;
4338        cx.simulate_shared_keystrokes("d v e").await;
4339        cx.shared_state().await.assert_eq(indoc! {"
4340              ˇe quick brown fox
4341              jumped over the lazy dog"});
4342        assert!(!cx.cx.forced_motion());
4343
4344        cx.set_shared_state(indoc! {"
4345              the quick bˇrown fox
4346              jumped over the lazy dog"})
4347            .await;
4348        cx.simulate_shared_keystrokes("d v e").await;
4349        cx.shared_state().await.assert_eq(indoc! {"
4350              the quick bˇn fox
4351              jumped over the lazy dog"});
4352        assert!(!cx.cx.forced_motion());
4353
4354        cx.set_shared_state(indoc! {"
4355             the quick brown foˇx
4356             jumped over the lazy dog"})
4357            .await;
4358        cx.simulate_shared_keystrokes("d v e").await;
4359        cx.shared_state().await.assert_eq(indoc! {"
4360        the quick brown foˇd over the lazy dog"});
4361        assert!(!cx.cx.forced_motion());
4362    }
4363}