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