motion.rs

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