motion.rs

   1use editor::{
   2    Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
   3    display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
   4    movement::{
   5        self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
   6    },
   7};
   8use gpui::{Action, Context, Window, actions, px};
   9use language::{CharKind, Point, Selection, SelectionGoal};
  10use multi_buffer::MultiBufferRow;
  11use schemars::JsonSchema;
  12use serde::Deserialize;
  13use std::ops::Range;
  14use workspace::searchable::Direction;
  15
  16use crate::{
  17    Vim,
  18    normal::mark,
  19    state::{Mode, Operator},
  20    surrounds::SurroundsType,
  21};
  22
  23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
  24pub(crate) enum MotionKind {
  25    Linewise,
  26    Exclusive,
  27    Inclusive,
  28}
  29
  30impl MotionKind {
  31    pub(crate) fn for_mode(mode: Mode) -> Self {
  32        match mode {
  33            Mode::VisualLine => MotionKind::Linewise,
  34            _ => MotionKind::Exclusive,
  35        }
  36    }
  37
  38    pub(crate) fn linewise(&self) -> bool {
  39        matches!(self, MotionKind::Linewise)
  40    }
  41}
  42
  43#[derive(Clone, Debug, PartialEq, Eq)]
  44pub enum Motion {
  45    Left,
  46    WrappingLeft,
  47    Down {
  48        display_lines: bool,
  49    },
  50    Up {
  51        display_lines: bool,
  52    },
  53    Right,
  54    WrappingRight,
  55    NextWordStart {
  56        ignore_punctuation: bool,
  57    },
  58    NextWordEnd {
  59        ignore_punctuation: bool,
  60    },
  61    PreviousWordStart {
  62        ignore_punctuation: bool,
  63    },
  64    PreviousWordEnd {
  65        ignore_punctuation: bool,
  66    },
  67    NextSubwordStart {
  68        ignore_punctuation: bool,
  69    },
  70    NextSubwordEnd {
  71        ignore_punctuation: bool,
  72    },
  73    PreviousSubwordStart {
  74        ignore_punctuation: bool,
  75    },
  76    PreviousSubwordEnd {
  77        ignore_punctuation: bool,
  78    },
  79    FirstNonWhitespace {
  80        display_lines: bool,
  81    },
  82    CurrentLine,
  83    StartOfLine {
  84        display_lines: bool,
  85    },
  86    MiddleOfLine {
  87        display_lines: bool,
  88    },
  89    EndOfLine {
  90        display_lines: bool,
  91    },
  92    SentenceBackward,
  93    SentenceForward,
  94    StartOfParagraph,
  95    EndOfParagraph,
  96    StartOfDocument,
  97    EndOfDocument,
  98    Matching,
  99    GoToPercentage,
 100    UnmatchedForward {
 101        char: char,
 102    },
 103    UnmatchedBackward {
 104        char: char,
 105    },
 106    FindForward {
 107        before: bool,
 108        char: char,
 109        mode: FindRange,
 110        smartcase: bool,
 111    },
 112    FindBackward {
 113        after: bool,
 114        char: char,
 115        mode: FindRange,
 116        smartcase: bool,
 117    },
 118    Sneak {
 119        first_char: char,
 120        second_char: char,
 121        smartcase: bool,
 122    },
 123    SneakBackward {
 124        first_char: char,
 125        second_char: char,
 126        smartcase: bool,
 127    },
 128    RepeatFind {
 129        last_find: Box<Motion>,
 130    },
 131    RepeatFindReversed {
 132        last_find: Box<Motion>,
 133    },
 134    NextLineStart,
 135    PreviousLineStart,
 136    StartOfLineDownward,
 137    EndOfLineDownward,
 138    GoToColumn,
 139    WindowTop,
 140    WindowMiddle,
 141    WindowBottom,
 142    NextSectionStart,
 143    NextSectionEnd,
 144    PreviousSectionStart,
 145    PreviousSectionEnd,
 146    NextMethodStart,
 147    NextMethodEnd,
 148    PreviousMethodStart,
 149    PreviousMethodEnd,
 150    NextComment,
 151    PreviousComment,
 152    PreviousLesserIndent,
 153    PreviousGreaterIndent,
 154    PreviousSameIndent,
 155    NextLesserIndent,
 156    NextGreaterIndent,
 157    NextSameIndent,
 158
 159    // we don't have a good way to run a search synchronously, so
 160    // we handle search motions by running the search async and then
 161    // calling back into motion with this
 162    ZedSearchResult {
 163        prior_selections: Vec<Range<Anchor>>,
 164        new_selections: Vec<Range<Anchor>>,
 165    },
 166    Jump {
 167        anchor: Anchor,
 168        line: bool,
 169    },
 170}
 171
 172#[derive(Clone, Copy)]
 173enum IndentType {
 174    Lesser,
 175    Greater,
 176    Same,
 177}
 178
 179/// Moves to the start of the next word.
 180#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 181#[action(namespace = vim)]
 182#[serde(deny_unknown_fields)]
 183struct NextWordStart {
 184    #[serde(default)]
 185    ignore_punctuation: bool,
 186}
 187
 188/// Moves to the end of the next word.
 189#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 190#[action(namespace = vim)]
 191#[serde(deny_unknown_fields)]
 192struct NextWordEnd {
 193    #[serde(default)]
 194    ignore_punctuation: bool,
 195}
 196
 197/// Moves to the start of the previous word.
 198#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 199#[action(namespace = vim)]
 200#[serde(deny_unknown_fields)]
 201struct PreviousWordStart {
 202    #[serde(default)]
 203    ignore_punctuation: bool,
 204}
 205
 206/// Moves to the end of the previous word.
 207#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 208#[action(namespace = vim)]
 209#[serde(deny_unknown_fields)]
 210struct PreviousWordEnd {
 211    #[serde(default)]
 212    ignore_punctuation: bool,
 213}
 214
 215/// Moves to the start of the next subword.
 216#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 217#[action(namespace = vim)]
 218#[serde(deny_unknown_fields)]
 219pub(crate) struct NextSubwordStart {
 220    #[serde(default)]
 221    pub(crate) ignore_punctuation: bool,
 222}
 223
 224/// Moves to the end of the next subword.
 225#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 226#[action(namespace = vim)]
 227#[serde(deny_unknown_fields)]
 228pub(crate) struct NextSubwordEnd {
 229    #[serde(default)]
 230    pub(crate) ignore_punctuation: bool,
 231}
 232
 233/// Moves to the start of the previous subword.
 234#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 235#[action(namespace = vim)]
 236#[serde(deny_unknown_fields)]
 237pub(crate) struct PreviousSubwordStart {
 238    #[serde(default)]
 239    pub(crate) ignore_punctuation: bool,
 240}
 241
 242/// Moves to the end of the previous subword.
 243#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 244#[action(namespace = vim)]
 245#[serde(deny_unknown_fields)]
 246pub(crate) struct PreviousSubwordEnd {
 247    #[serde(default)]
 248    pub(crate) ignore_punctuation: bool,
 249}
 250
 251/// Moves cursor up by the specified number of lines.
 252#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 253#[action(namespace = vim)]
 254#[serde(deny_unknown_fields)]
 255pub(crate) struct Up {
 256    #[serde(default)]
 257    pub(crate) display_lines: bool,
 258}
 259
 260/// Moves cursor down by the specified number of lines.
 261#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 262#[action(namespace = vim)]
 263#[serde(deny_unknown_fields)]
 264pub(crate) struct Down {
 265    #[serde(default)]
 266    pub(crate) display_lines: bool,
 267}
 268
 269/// Moves to the first non-whitespace character on the current line.
 270#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 271#[action(namespace = vim)]
 272#[serde(deny_unknown_fields)]
 273struct FirstNonWhitespace {
 274    #[serde(default)]
 275    display_lines: bool,
 276}
 277
 278/// Moves to the end of the current line.
 279#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 280#[action(namespace = vim)]
 281#[serde(deny_unknown_fields)]
 282struct EndOfLine {
 283    #[serde(default)]
 284    display_lines: bool,
 285}
 286
 287/// Moves to the start of the current line.
 288#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 289#[action(namespace = vim)]
 290#[serde(deny_unknown_fields)]
 291pub struct StartOfLine {
 292    #[serde(default)]
 293    pub(crate) display_lines: bool,
 294}
 295
 296/// Moves to the middle of the current line.
 297#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 298#[action(namespace = vim)]
 299#[serde(deny_unknown_fields)]
 300struct MiddleOfLine {
 301    #[serde(default)]
 302    display_lines: bool,
 303}
 304
 305/// Finds the next unmatched bracket or delimiter.
 306#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 307#[action(namespace = vim)]
 308#[serde(deny_unknown_fields)]
 309struct UnmatchedForward {
 310    #[serde(default)]
 311    char: char,
 312}
 313
 314/// Finds the previous unmatched bracket or delimiter.
 315#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 316#[action(namespace = vim)]
 317#[serde(deny_unknown_fields)]
 318struct UnmatchedBackward {
 319    #[serde(default)]
 320    char: char,
 321}
 322
 323actions!(
 324    vim,
 325    [
 326        /// Moves cursor left one character.
 327        Left,
 328        /// Moves cursor left one character, wrapping to previous line.
 329        #[action(deprecated_aliases = ["vim::Backspace"])]
 330        WrappingLeft,
 331        /// Moves cursor right one character.
 332        Right,
 333        /// Moves cursor right one character, wrapping to next line.
 334        #[action(deprecated_aliases = ["vim::Space"])]
 335        WrappingRight,
 336        /// Selects the current line.
 337        CurrentLine,
 338        /// Moves to the start of the next sentence.
 339        SentenceForward,
 340        /// Moves to the start of the previous sentence.
 341        SentenceBackward,
 342        /// Moves to the start of the paragraph.
 343        StartOfParagraph,
 344        /// Moves to the end of the paragraph.
 345        EndOfParagraph,
 346        /// Moves to the start of the document.
 347        StartOfDocument,
 348        /// Moves to the end of the document.
 349        EndOfDocument,
 350        /// Moves to the matching bracket or delimiter.
 351        Matching,
 352        /// Goes to a percentage position in the file.
 353        GoToPercentage,
 354        /// Moves to the start of the next line.
 355        NextLineStart,
 356        /// Moves to the start of the previous line.
 357        PreviousLineStart,
 358        /// Moves to the start of a line downward.
 359        StartOfLineDownward,
 360        /// Moves to the end of a line downward.
 361        EndOfLineDownward,
 362        /// Goes to a specific column number.
 363        GoToColumn,
 364        /// Repeats the last character find.
 365        RepeatFind,
 366        /// Repeats the last character find in reverse.
 367        RepeatFindReversed,
 368        /// Moves to the top of the window.
 369        WindowTop,
 370        /// Moves to the middle of the window.
 371        WindowMiddle,
 372        /// Moves to the bottom of the window.
 373        WindowBottom,
 374        /// Moves to the start of the next section.
 375        NextSectionStart,
 376        /// Moves to the end of the next section.
 377        NextSectionEnd,
 378        /// Moves to the start of the previous section.
 379        PreviousSectionStart,
 380        /// Moves to the end of the previous section.
 381        PreviousSectionEnd,
 382        /// Moves to the start of the next method.
 383        NextMethodStart,
 384        /// Moves to the end of the next method.
 385        NextMethodEnd,
 386        /// Moves to the start of the previous method.
 387        PreviousMethodStart,
 388        /// Moves to the end of the previous method.
 389        PreviousMethodEnd,
 390        /// Moves to the next comment.
 391        NextComment,
 392        /// Moves to the previous comment.
 393        PreviousComment,
 394        /// Moves to the previous line with lesser indentation.
 395        PreviousLesserIndent,
 396        /// Moves to the previous line with greater indentation.
 397        PreviousGreaterIndent,
 398        /// Moves to the previous line with the same indentation.
 399        PreviousSameIndent,
 400        /// Moves to the next line with lesser indentation.
 401        NextLesserIndent,
 402        /// Moves to the next line with greater indentation.
 403        NextGreaterIndent,
 404        /// Moves to the next line with the same indentation.
 405        NextSameIndent,
 406    ]
 407);
 408
 409pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 410    Vim::action(editor, cx, |vim, _: &Left, window, cx| {
 411        vim.motion(Motion::Left, window, cx)
 412    });
 413    Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
 414        vim.motion(Motion::WrappingLeft, window, cx)
 415    });
 416    Vim::action(editor, cx, |vim, action: &Down, window, cx| {
 417        vim.motion(
 418            Motion::Down {
 419                display_lines: action.display_lines,
 420            },
 421            window,
 422            cx,
 423        )
 424    });
 425    Vim::action(editor, cx, |vim, action: &Up, window, cx| {
 426        vim.motion(
 427            Motion::Up {
 428                display_lines: action.display_lines,
 429            },
 430            window,
 431            cx,
 432        )
 433    });
 434    Vim::action(editor, cx, |vim, _: &Right, window, cx| {
 435        vim.motion(Motion::Right, window, cx)
 436    });
 437    Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
 438        vim.motion(Motion::WrappingRight, window, cx)
 439    });
 440    Vim::action(
 441        editor,
 442        cx,
 443        |vim, action: &FirstNonWhitespace, window, cx| {
 444            vim.motion(
 445                Motion::FirstNonWhitespace {
 446                    display_lines: action.display_lines,
 447                },
 448                window,
 449                cx,
 450            )
 451        },
 452    );
 453    Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
 454        vim.motion(
 455            Motion::StartOfLine {
 456                display_lines: action.display_lines,
 457            },
 458            window,
 459            cx,
 460        )
 461    });
 462    Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| {
 463        vim.motion(
 464            Motion::MiddleOfLine {
 465                display_lines: action.display_lines,
 466            },
 467            window,
 468            cx,
 469        )
 470    });
 471    Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
 472        vim.motion(
 473            Motion::EndOfLine {
 474                display_lines: action.display_lines,
 475            },
 476            window,
 477            cx,
 478        )
 479    });
 480    Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
 481        vim.motion(Motion::CurrentLine, window, cx)
 482    });
 483    Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
 484        vim.motion(Motion::StartOfParagraph, window, cx)
 485    });
 486    Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
 487        vim.motion(Motion::EndOfParagraph, window, cx)
 488    });
 489
 490    Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
 491        vim.motion(Motion::SentenceForward, window, cx)
 492    });
 493    Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
 494        vim.motion(Motion::SentenceBackward, window, cx)
 495    });
 496    Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
 497        vim.motion(Motion::StartOfDocument, window, cx)
 498    });
 499    Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
 500        vim.motion(Motion::EndOfDocument, window, cx)
 501    });
 502    Vim::action(editor, cx, |vim, _: &Matching, window, cx| {
 503        vim.motion(Motion::Matching, window, cx)
 504    });
 505    Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
 506        vim.motion(Motion::GoToPercentage, window, cx)
 507    });
 508    Vim::action(
 509        editor,
 510        cx,
 511        |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
 512            vim.motion(Motion::UnmatchedForward { char }, window, cx)
 513        },
 514    );
 515    Vim::action(
 516        editor,
 517        cx,
 518        |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
 519            vim.motion(Motion::UnmatchedBackward { char }, window, cx)
 520        },
 521    );
 522    Vim::action(
 523        editor,
 524        cx,
 525        |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
 526            vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
 527        },
 528    );
 529    Vim::action(
 530        editor,
 531        cx,
 532        |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
 533            vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
 534        },
 535    );
 536    Vim::action(
 537        editor,
 538        cx,
 539        |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
 540            vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
 541        },
 542    );
 543    Vim::action(
 544        editor,
 545        cx,
 546        |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
 547            vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
 548        },
 549    );
 550    Vim::action(
 551        editor,
 552        cx,
 553        |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
 554            vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
 555        },
 556    );
 557    Vim::action(
 558        editor,
 559        cx,
 560        |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
 561            vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
 562        },
 563    );
 564    Vim::action(
 565        editor,
 566        cx,
 567        |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
 568            vim.motion(
 569                Motion::PreviousSubwordStart { ignore_punctuation },
 570                window,
 571                cx,
 572            )
 573        },
 574    );
 575    Vim::action(
 576        editor,
 577        cx,
 578        |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
 579            vim.motion(
 580                Motion::PreviousSubwordEnd { ignore_punctuation },
 581                window,
 582                cx,
 583            )
 584        },
 585    );
 586    Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
 587        vim.motion(Motion::NextLineStart, window, cx)
 588    });
 589    Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
 590        vim.motion(Motion::PreviousLineStart, window, cx)
 591    });
 592    Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
 593        vim.motion(Motion::StartOfLineDownward, window, cx)
 594    });
 595    Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
 596        vim.motion(Motion::EndOfLineDownward, window, cx)
 597    });
 598    Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
 599        vim.motion(Motion::GoToColumn, window, cx)
 600    });
 601
 602    Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
 603        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 604            vim.motion(Motion::RepeatFind { last_find }, window, cx);
 605        }
 606    });
 607
 608    Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
 609        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 610            vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
 611        }
 612    });
 613    Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
 614        vim.motion(Motion::WindowTop, window, cx)
 615    });
 616    Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
 617        vim.motion(Motion::WindowMiddle, window, cx)
 618    });
 619    Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
 620        vim.motion(Motion::WindowBottom, window, cx)
 621    });
 622
 623    Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
 624        vim.motion(Motion::PreviousSectionStart, window, cx)
 625    });
 626    Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
 627        vim.motion(Motion::NextSectionStart, window, cx)
 628    });
 629    Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
 630        vim.motion(Motion::PreviousSectionEnd, window, cx)
 631    });
 632    Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
 633        vim.motion(Motion::NextSectionEnd, window, cx)
 634    });
 635    Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
 636        vim.motion(Motion::PreviousMethodStart, window, cx)
 637    });
 638    Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
 639        vim.motion(Motion::NextMethodStart, window, cx)
 640    });
 641    Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
 642        vim.motion(Motion::PreviousMethodEnd, window, cx)
 643    });
 644    Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
 645        vim.motion(Motion::NextMethodEnd, window, cx)
 646    });
 647    Vim::action(editor, cx, |vim, &NextComment, window, cx| {
 648        vim.motion(Motion::NextComment, window, cx)
 649    });
 650    Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
 651        vim.motion(Motion::PreviousComment, window, cx)
 652    });
 653    Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| {
 654        vim.motion(Motion::PreviousLesserIndent, window, cx)
 655    });
 656    Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| {
 657        vim.motion(Motion::PreviousGreaterIndent, window, cx)
 658    });
 659    Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| {
 660        vim.motion(Motion::PreviousSameIndent, window, cx)
 661    });
 662    Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| {
 663        vim.motion(Motion::NextLesserIndent, window, cx)
 664    });
 665    Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| {
 666        vim.motion(Motion::NextGreaterIndent, window, cx)
 667    });
 668    Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| {
 669        vim.motion(Motion::NextSameIndent, window, cx)
 670    });
 671}
 672
 673impl Vim {
 674    pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
 675        if let Motion::ZedSearchResult {
 676            prior_selections, ..
 677        } = &m
 678        {
 679            match self.mode {
 680                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 681                    if !prior_selections.is_empty() {
 682                        self.update_editor(cx, |_, editor, cx| {
 683                            editor.change_selections(Default::default(), window, cx, |s| {
 684                                s.select_ranges(prior_selections.iter().cloned())
 685                            })
 686                        });
 687                    }
 688                }
 689                Mode::Normal | Mode::Replace | Mode::Insert => {
 690                    if self.active_operator().is_none() {
 691                        return;
 692                    }
 693                }
 694
 695                Mode::HelixNormal => {}
 696            }
 697        }
 698
 699        self.motion(m, window, cx)
 700    }
 701
 702    pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context<Self>) {
 703        if let Some(Operator::FindForward { .. })
 704        | Some(Operator::Sneak { .. })
 705        | Some(Operator::SneakBackward { .. })
 706        | Some(Operator::FindBackward { .. }) = self.active_operator()
 707        {
 708            self.pop_operator(window, cx);
 709        }
 710
 711        let count = Vim::take_count(cx);
 712        let forced_motion = Vim::take_forced_motion(cx);
 713        let active_operator = self.active_operator();
 714        let mut waiting_operator: Option<Operator> = None;
 715        match self.mode {
 716            Mode::Normal | Mode::Replace | Mode::Insert => {
 717                if active_operator == Some(Operator::AddSurrounds { target: None }) {
 718                    waiting_operator = Some(Operator::AddSurrounds {
 719                        target: Some(SurroundsType::Motion(motion)),
 720                    });
 721                } else {
 722                    self.normal_motion(
 723                        motion.clone(),
 724                        active_operator.clone(),
 725                        count,
 726                        forced_motion,
 727                        window,
 728                        cx,
 729                    )
 730                }
 731            }
 732            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 733                self.visual_motion(motion.clone(), count, window, cx)
 734            }
 735
 736            Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, window, cx),
 737        }
 738        self.clear_operator(window, cx);
 739        if let Some(operator) = waiting_operator {
 740            self.push_operator(operator, window, cx);
 741            Vim::globals(cx).pre_count = count
 742        }
 743    }
 744}
 745
 746// Motion handling is specified here:
 747// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 748impl Motion {
 749    fn default_kind(&self) -> MotionKind {
 750        use Motion::*;
 751        match self {
 752            Down { .. }
 753            | Up { .. }
 754            | StartOfDocument
 755            | EndOfDocument
 756            | CurrentLine
 757            | NextLineStart
 758            | PreviousLineStart
 759            | StartOfLineDownward
 760            | WindowTop
 761            | WindowMiddle
 762            | WindowBottom
 763            | NextSectionStart
 764            | NextSectionEnd
 765            | PreviousSectionStart
 766            | PreviousSectionEnd
 767            | NextMethodStart
 768            | NextMethodEnd
 769            | PreviousMethodStart
 770            | PreviousMethodEnd
 771            | NextComment
 772            | PreviousComment
 773            | PreviousLesserIndent
 774            | PreviousGreaterIndent
 775            | PreviousSameIndent
 776            | NextLesserIndent
 777            | NextGreaterIndent
 778            | NextSameIndent
 779            | GoToPercentage
 780            | Jump { line: true, .. } => MotionKind::Linewise,
 781            EndOfLine { .. }
 782            | EndOfLineDownward
 783            | Matching
 784            | FindForward { .. }
 785            | NextWordEnd { .. }
 786            | PreviousWordEnd { .. }
 787            | NextSubwordEnd { .. }
 788            | PreviousSubwordEnd { .. } => MotionKind::Inclusive,
 789            Left
 790            | WrappingLeft
 791            | Right
 792            | WrappingRight
 793            | StartOfLine { .. }
 794            | StartOfParagraph
 795            | EndOfParagraph
 796            | SentenceBackward
 797            | SentenceForward
 798            | GoToColumn
 799            | MiddleOfLine { .. }
 800            | UnmatchedForward { .. }
 801            | UnmatchedBackward { .. }
 802            | NextWordStart { .. }
 803            | PreviousWordStart { .. }
 804            | NextSubwordStart { .. }
 805            | PreviousSubwordStart { .. }
 806            | FirstNonWhitespace { .. }
 807            | FindBackward { .. }
 808            | Sneak { .. }
 809            | SneakBackward { .. }
 810            | Jump { .. }
 811            | ZedSearchResult { .. } => MotionKind::Exclusive,
 812            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 813                motion.default_kind()
 814            }
 815        }
 816    }
 817
 818    fn skip_exclusive_special_case(&self) -> bool {
 819        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, 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    always_advance: bool,
1727) -> DisplayPoint {
1728    let classifier = map
1729        .buffer_snapshot
1730        .char_classifier_at(point.to_point(map))
1731        .ignore_punctuation(ignore_punctuation);
1732    for _ in 0..times {
1733        let mut need_next_char = false;
1734        let new_point = if always_advance {
1735            next_char(map, point, allow_cross_newline)
1736        } else {
1737            point
1738        };
1739        let new_point = movement::find_boundary_exclusive(
1740            map,
1741            new_point,
1742            FindRange::MultiLine,
1743            |left, right| {
1744                let left_kind = classifier.kind(left);
1745                let right_kind = classifier.kind(right);
1746                let at_newline = right == '\n';
1747
1748                if !allow_cross_newline && at_newline {
1749                    need_next_char = true;
1750                    return true;
1751                }
1752
1753                left_kind != right_kind && left_kind != CharKind::Whitespace
1754            },
1755        );
1756        let new_point = if need_next_char {
1757            next_char(map, new_point, true)
1758        } else {
1759            new_point
1760        };
1761        let new_point = map.clip_point(new_point, Bias::Left);
1762        if point == new_point {
1763            break;
1764        }
1765        point = new_point;
1766    }
1767    point
1768}
1769
1770fn previous_word_start(
1771    map: &DisplaySnapshot,
1772    mut point: DisplayPoint,
1773    ignore_punctuation: bool,
1774    times: usize,
1775) -> DisplayPoint {
1776    let classifier = map
1777        .buffer_snapshot
1778        .char_classifier_at(point.to_point(map))
1779        .ignore_punctuation(ignore_punctuation);
1780    for _ in 0..times {
1781        // This works even though find_preceding_boundary is called for every character in the line containing
1782        // cursor because the newline is checked only once.
1783        let new_point = movement::find_preceding_boundary_display_point(
1784            map,
1785            point,
1786            FindRange::MultiLine,
1787            |left, right| {
1788                let left_kind = classifier.kind(left);
1789                let right_kind = classifier.kind(right);
1790
1791                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1792            },
1793        );
1794        if point == new_point {
1795            break;
1796        }
1797        point = new_point;
1798    }
1799    point
1800}
1801
1802fn previous_word_end(
1803    map: &DisplaySnapshot,
1804    point: DisplayPoint,
1805    ignore_punctuation: bool,
1806    times: usize,
1807) -> DisplayPoint {
1808    let classifier = map
1809        .buffer_snapshot
1810        .char_classifier_at(point.to_point(map))
1811        .ignore_punctuation(ignore_punctuation);
1812    let mut point = point.to_point(map);
1813
1814    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1815        if let Some(ch) = map.buffer_snapshot.chars_at(point).next() {
1816            point.column += ch.len_utf8() as u32;
1817        }
1818    }
1819    for _ in 0..times {
1820        let new_point = movement::find_preceding_boundary_point(
1821            &map.buffer_snapshot,
1822            point,
1823            FindRange::MultiLine,
1824            |left, right| {
1825                let left_kind = classifier.kind(left);
1826                let right_kind = classifier.kind(right);
1827                match (left_kind, right_kind) {
1828                    (CharKind::Punctuation, CharKind::Whitespace)
1829                    | (CharKind::Punctuation, CharKind::Word)
1830                    | (CharKind::Word, CharKind::Whitespace)
1831                    | (CharKind::Word, CharKind::Punctuation) => true,
1832                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1833                    _ => false,
1834                }
1835            },
1836        );
1837        if new_point == point {
1838            break;
1839        }
1840        point = new_point;
1841    }
1842    movement::saturating_left(map, point.to_display_point(map))
1843}
1844
1845fn next_subword_start(
1846    map: &DisplaySnapshot,
1847    mut point: DisplayPoint,
1848    ignore_punctuation: bool,
1849    times: usize,
1850) -> DisplayPoint {
1851    let classifier = map
1852        .buffer_snapshot
1853        .char_classifier_at(point.to_point(map))
1854        .ignore_punctuation(ignore_punctuation);
1855    for _ in 0..times {
1856        let mut crossed_newline = false;
1857        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1858            let left_kind = classifier.kind(left);
1859            let right_kind = classifier.kind(right);
1860            let at_newline = right == '\n';
1861
1862            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1863            let is_subword_start =
1864                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1865
1866            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1867                || at_newline && crossed_newline
1868                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1869
1870            crossed_newline |= at_newline;
1871            found
1872        });
1873        if point == new_point {
1874            break;
1875        }
1876        point = new_point;
1877    }
1878    point
1879}
1880
1881pub(crate) fn next_subword_end(
1882    map: &DisplaySnapshot,
1883    mut point: DisplayPoint,
1884    ignore_punctuation: bool,
1885    times: usize,
1886    allow_cross_newline: bool,
1887) -> DisplayPoint {
1888    let classifier = map
1889        .buffer_snapshot
1890        .char_classifier_at(point.to_point(map))
1891        .ignore_punctuation(ignore_punctuation);
1892    for _ in 0..times {
1893        let new_point = next_char(map, point, allow_cross_newline);
1894
1895        let mut crossed_newline = false;
1896        let mut need_backtrack = false;
1897        let new_point =
1898            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1899                let left_kind = classifier.kind(left);
1900                let right_kind = classifier.kind(right);
1901                let at_newline = right == '\n';
1902
1903                if !allow_cross_newline && at_newline {
1904                    return true;
1905                }
1906
1907                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1908                let is_subword_end =
1909                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1910
1911                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1912
1913                if found && (is_word_end || is_subword_end) {
1914                    need_backtrack = true;
1915                }
1916
1917                crossed_newline |= at_newline;
1918                found
1919            });
1920        let mut new_point = map.clip_point(new_point, Bias::Left);
1921        if need_backtrack {
1922            *new_point.column_mut() -= 1;
1923        }
1924        let new_point = map.clip_point(new_point, Bias::Left);
1925        if point == new_point {
1926            break;
1927        }
1928        point = new_point;
1929    }
1930    point
1931}
1932
1933fn previous_subword_start(
1934    map: &DisplaySnapshot,
1935    mut point: DisplayPoint,
1936    ignore_punctuation: bool,
1937    times: usize,
1938) -> DisplayPoint {
1939    let classifier = map
1940        .buffer_snapshot
1941        .char_classifier_at(point.to_point(map))
1942        .ignore_punctuation(ignore_punctuation);
1943    for _ in 0..times {
1944        let mut crossed_newline = false;
1945        // This works even though find_preceding_boundary is called for every character in the line containing
1946        // cursor because the newline is checked only once.
1947        let new_point = movement::find_preceding_boundary_display_point(
1948            map,
1949            point,
1950            FindRange::MultiLine,
1951            |left, right| {
1952                let left_kind = classifier.kind(left);
1953                let right_kind = classifier.kind(right);
1954                let at_newline = right == '\n';
1955
1956                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1957                let is_subword_start =
1958                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1959
1960                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1961                    || at_newline && crossed_newline
1962                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1963
1964                crossed_newline |= at_newline;
1965
1966                found
1967            },
1968        );
1969        if point == new_point {
1970            break;
1971        }
1972        point = new_point;
1973    }
1974    point
1975}
1976
1977fn previous_subword_end(
1978    map: &DisplaySnapshot,
1979    point: DisplayPoint,
1980    ignore_punctuation: bool,
1981    times: usize,
1982) -> DisplayPoint {
1983    let classifier = map
1984        .buffer_snapshot
1985        .char_classifier_at(point.to_point(map))
1986        .ignore_punctuation(ignore_punctuation);
1987    let mut point = point.to_point(map);
1988
1989    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1990        if let Some(ch) = map.buffer_snapshot.chars_at(point).next() {
1991            point.column += ch.len_utf8() as u32;
1992        }
1993    }
1994    for _ in 0..times {
1995        let new_point = movement::find_preceding_boundary_point(
1996            &map.buffer_snapshot,
1997            point,
1998            FindRange::MultiLine,
1999            |left, right| {
2000                let left_kind = classifier.kind(left);
2001                let right_kind = classifier.kind(right);
2002
2003                let is_subword_end =
2004                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
2005
2006                if is_subword_end {
2007                    return true;
2008                }
2009
2010                match (left_kind, right_kind) {
2011                    (CharKind::Word, CharKind::Whitespace)
2012                    | (CharKind::Word, CharKind::Punctuation) => true,
2013                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2014                    _ => false,
2015                }
2016            },
2017        );
2018        if new_point == point {
2019            break;
2020        }
2021        point = new_point;
2022    }
2023    movement::saturating_left(map, point.to_display_point(map))
2024}
2025
2026pub(crate) fn first_non_whitespace(
2027    map: &DisplaySnapshot,
2028    display_lines: bool,
2029    from: DisplayPoint,
2030) -> DisplayPoint {
2031    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2032    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2033    for (ch, offset) in map.buffer_chars_at(start_offset) {
2034        if ch == '\n' {
2035            return from;
2036        }
2037
2038        start_offset = offset;
2039
2040        if classifier.kind(ch) != CharKind::Whitespace {
2041            break;
2042        }
2043    }
2044
2045    start_offset.to_display_point(map)
2046}
2047
2048pub(crate) fn last_non_whitespace(
2049    map: &DisplaySnapshot,
2050    from: DisplayPoint,
2051    count: usize,
2052) -> DisplayPoint {
2053    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2054    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2055
2056    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2057    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
2058        if classifier.kind(ch) != CharKind::Whitespace {
2059            return end_of_line.to_display_point(map);
2060        }
2061    }
2062
2063    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2064        if ch == '\n' {
2065            break;
2066        }
2067        end_of_line = offset;
2068        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2069            break;
2070        }
2071    }
2072
2073    end_of_line.to_display_point(map)
2074}
2075
2076pub(crate) fn start_of_line(
2077    map: &DisplaySnapshot,
2078    display_lines: bool,
2079    point: DisplayPoint,
2080) -> DisplayPoint {
2081    if display_lines {
2082        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2083    } else {
2084        map.prev_line_boundary(point.to_point(map)).1
2085    }
2086}
2087
2088pub(crate) fn middle_of_line(
2089    map: &DisplaySnapshot,
2090    display_lines: bool,
2091    point: DisplayPoint,
2092    times: Option<usize>,
2093) -> DisplayPoint {
2094    let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2095        times as f64 / 100.
2096    } else {
2097        0.5
2098    };
2099    if display_lines {
2100        map.clip_point(
2101            DisplayPoint::new(
2102                point.row(),
2103                (map.line_len(point.row()) as f64 * percent) as u32,
2104            ),
2105            Bias::Left,
2106        )
2107    } else {
2108        let mut buffer_point = point.to_point(map);
2109        buffer_point.column = (map
2110            .buffer_snapshot
2111            .line_len(MultiBufferRow(buffer_point.row)) as f64
2112            * percent) as u32;
2113
2114        map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2115    }
2116}
2117
2118pub(crate) fn end_of_line(
2119    map: &DisplaySnapshot,
2120    display_lines: bool,
2121    mut point: DisplayPoint,
2122    times: usize,
2123) -> DisplayPoint {
2124    if times > 1 {
2125        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2126    }
2127    if display_lines {
2128        map.clip_point(
2129            DisplayPoint::new(point.row(), map.line_len(point.row())),
2130            Bias::Left,
2131        )
2132    } else {
2133        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2134    }
2135}
2136
2137pub(crate) fn sentence_backwards(
2138    map: &DisplaySnapshot,
2139    point: DisplayPoint,
2140    mut times: usize,
2141) -> DisplayPoint {
2142    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2143    let mut chars = map.reverse_buffer_chars_at(start).peekable();
2144
2145    let mut was_newline = map
2146        .buffer_chars_at(start)
2147        .next()
2148        .is_some_and(|(c, _)| c == '\n');
2149
2150    while let Some((ch, offset)) = chars.next() {
2151        let start_of_next_sentence = if was_newline && ch == '\n' {
2152            Some(offset + ch.len_utf8())
2153        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2154            Some(next_non_blank(map, offset + ch.len_utf8()))
2155        } else if ch == '.' || ch == '?' || ch == '!' {
2156            start_of_next_sentence(map, offset + ch.len_utf8())
2157        } else {
2158            None
2159        };
2160
2161        if let Some(start_of_next_sentence) = start_of_next_sentence {
2162            if start_of_next_sentence < start {
2163                times = times.saturating_sub(1);
2164            }
2165            if times == 0 || offset == 0 {
2166                return map.clip_point(
2167                    start_of_next_sentence
2168                        .to_offset(&map.buffer_snapshot)
2169                        .to_display_point(map),
2170                    Bias::Left,
2171                );
2172            }
2173        }
2174        if was_newline {
2175            start = offset;
2176        }
2177        was_newline = ch == '\n';
2178    }
2179
2180    DisplayPoint::zero()
2181}
2182
2183pub(crate) fn sentence_forwards(
2184    map: &DisplaySnapshot,
2185    point: DisplayPoint,
2186    mut times: usize,
2187) -> DisplayPoint {
2188    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2189    let mut chars = map.buffer_chars_at(start).peekable();
2190
2191    let mut was_newline = map
2192        .reverse_buffer_chars_at(start)
2193        .next()
2194        .is_some_and(|(c, _)| c == '\n')
2195        && chars.peek().is_some_and(|(c, _)| *c == '\n');
2196
2197    while let Some((ch, offset)) = chars.next() {
2198        if was_newline && ch == '\n' {
2199            continue;
2200        }
2201        let start_of_next_sentence = if was_newline {
2202            Some(next_non_blank(map, offset))
2203        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2204            Some(next_non_blank(map, offset + ch.len_utf8()))
2205        } else if ch == '.' || ch == '?' || ch == '!' {
2206            start_of_next_sentence(map, offset + ch.len_utf8())
2207        } else {
2208            None
2209        };
2210
2211        if let Some(start_of_next_sentence) = start_of_next_sentence {
2212            times = times.saturating_sub(1);
2213            if times == 0 {
2214                return map.clip_point(
2215                    start_of_next_sentence
2216                        .to_offset(&map.buffer_snapshot)
2217                        .to_display_point(map),
2218                    Bias::Right,
2219                );
2220            }
2221        }
2222
2223        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2224    }
2225
2226    map.max_point()
2227}
2228
2229fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2230    for (c, o) in map.buffer_chars_at(start) {
2231        if c == '\n' || !c.is_whitespace() {
2232            return o;
2233        }
2234    }
2235
2236    map.buffer_snapshot.len()
2237}
2238
2239// given the offset after a ., !, or ? find the start of the next sentence.
2240// if this is not a sentence boundary, returns None.
2241fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2242    let chars = map.buffer_chars_at(end_of_sentence);
2243    let mut seen_space = false;
2244
2245    for (char, offset) in chars {
2246        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2247            continue;
2248        }
2249
2250        if char == '\n' && seen_space {
2251            return Some(offset);
2252        } else if char.is_whitespace() {
2253            seen_space = true;
2254        } else if seen_space {
2255            return Some(offset);
2256        } else {
2257            return None;
2258        }
2259    }
2260
2261    Some(map.buffer_snapshot.len())
2262}
2263
2264fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2265    let point = map.display_point_to_point(display_point, Bias::Left);
2266    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2267        return display_point;
2268    };
2269    let offset = excerpt.buffer().point_to_offset(
2270        excerpt
2271            .buffer()
2272            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2273    );
2274    let buffer_range = excerpt.buffer_range();
2275    if offset >= buffer_range.start && offset <= buffer_range.end {
2276        let point = map
2277            .buffer_snapshot
2278            .offset_to_point(excerpt.map_offset_from_buffer(offset));
2279        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2280    }
2281    let mut last_position = None;
2282    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2283        let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2284            ..language::ToOffset::to_offset(&range.context.end, &buffer);
2285        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2286            let text_anchor = buffer.anchor_after(offset);
2287            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2288            return anchor.to_display_point(map);
2289        } else if offset <= excerpt_range.start {
2290            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2291            return anchor.to_display_point(map);
2292        } else {
2293            last_position = Some(Anchor::in_buffer(
2294                excerpt,
2295                buffer.remote_id(),
2296                range.context.end,
2297            ));
2298        }
2299    }
2300
2301    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2302    last_point.column = point.column;
2303
2304    map.clip_point(
2305        map.point_to_display_point(
2306            map.buffer_snapshot.clip_point(point, Bias::Left),
2307            Bias::Left,
2308        ),
2309        Bias::Left,
2310    )
2311}
2312
2313fn start_of_document(
2314    map: &DisplaySnapshot,
2315    display_point: DisplayPoint,
2316    maybe_times: Option<usize>,
2317) -> DisplayPoint {
2318    if let Some(times) = maybe_times {
2319        return go_to_line(map, display_point, times);
2320    }
2321
2322    let point = map.display_point_to_point(display_point, Bias::Left);
2323    let mut first_point = Point::zero();
2324    first_point.column = point.column;
2325
2326    map.clip_point(
2327        map.point_to_display_point(
2328            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2329            Bias::Left,
2330        ),
2331        Bias::Left,
2332    )
2333}
2334
2335fn end_of_document(
2336    map: &DisplaySnapshot,
2337    display_point: DisplayPoint,
2338    maybe_times: Option<usize>,
2339) -> DisplayPoint {
2340    if let Some(times) = maybe_times {
2341        return go_to_line(map, display_point, times);
2342    };
2343    let point = map.display_point_to_point(display_point, Bias::Left);
2344    let mut last_point = map.buffer_snapshot.max_point();
2345    last_point.column = point.column;
2346
2347    map.clip_point(
2348        map.point_to_display_point(
2349            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2350            Bias::Left,
2351        ),
2352        Bias::Left,
2353    )
2354}
2355
2356fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2357    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2358    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2359
2360    if head > outer.start && head < inner.start {
2361        let mut offset = inner.end.to_offset(map, Bias::Left);
2362        for c in map.buffer_snapshot.chars_at(offset) {
2363            if c == '/' || c == '\n' || c == '>' {
2364                return Some(offset.to_display_point(map));
2365            }
2366            offset += c.len_utf8();
2367        }
2368    } else {
2369        let mut offset = outer.start.to_offset(map, Bias::Left);
2370        for c in map.buffer_snapshot.chars_at(offset) {
2371            offset += c.len_utf8();
2372            if c == '<' || c == '\n' {
2373                return Some(offset.to_display_point(map));
2374            }
2375        }
2376    }
2377
2378    return None;
2379}
2380
2381fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2382    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2383    let display_point = map.clip_at_line_end(display_point);
2384    let point = display_point.to_point(map);
2385    let offset = point.to_offset(&map.buffer_snapshot);
2386
2387    // Ensure the range is contained by the current line.
2388    let mut line_end = map.next_line_boundary(point).0;
2389    if line_end == point {
2390        line_end = map.max_point().to_point(map);
2391    }
2392
2393    if let Some((opening_range, closing_range)) = map
2394        .buffer_snapshot
2395        .innermost_enclosing_bracket_ranges(offset..offset, None)
2396    {
2397        if opening_range.contains(&offset) {
2398            return closing_range.start.to_display_point(map);
2399        } else if closing_range.contains(&offset) {
2400            return opening_range.start.to_display_point(map);
2401        }
2402    }
2403
2404    let line_range = map.prev_line_boundary(point).0..line_end;
2405    let visible_line_range =
2406        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2407    let ranges = map
2408        .buffer_snapshot
2409        .bracket_ranges(visible_line_range.clone());
2410    if let Some(ranges) = ranges {
2411        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2412            ..line_range.end.to_offset(&map.buffer_snapshot);
2413        let mut closest_pair_destination = None;
2414        let mut closest_distance = usize::MAX;
2415
2416        for (open_range, close_range) in ranges {
2417            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2418                if offset > open_range.start && offset < close_range.start {
2419                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2420                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2421                        return display_point;
2422                    }
2423                    if let Some(tag) = matching_tag(map, display_point) {
2424                        return tag;
2425                    }
2426                } else if close_range.contains(&offset) {
2427                    return open_range.start.to_display_point(map);
2428                } else if open_range.contains(&offset) {
2429                    return (close_range.end - 1).to_display_point(map);
2430                }
2431            }
2432
2433            if (open_range.contains(&offset) || open_range.start >= offset)
2434                && line_range.contains(&open_range.start)
2435            {
2436                let distance = open_range.start.saturating_sub(offset);
2437                if distance < closest_distance {
2438                    closest_pair_destination = Some(close_range.start);
2439                    closest_distance = distance;
2440                    continue;
2441                }
2442            }
2443
2444            if (close_range.contains(&offset) || close_range.start >= offset)
2445                && line_range.contains(&close_range.start)
2446            {
2447                let distance = close_range.start.saturating_sub(offset);
2448                if distance < closest_distance {
2449                    closest_pair_destination = Some(open_range.start);
2450                    closest_distance = distance;
2451                    continue;
2452                }
2453            }
2454
2455            continue;
2456        }
2457
2458        closest_pair_destination
2459            .map(|destination| destination.to_display_point(map))
2460            .unwrap_or(display_point)
2461    } else {
2462        display_point
2463    }
2464}
2465
2466// Go to {count} percentage in the file, on the first
2467// non-blank in the line linewise.  To compute the new
2468// line number this formula is used:
2469// ({count} * number-of-lines + 99) / 100
2470//
2471// https://neovim.io/doc/user/motion.html#N%25
2472fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2473    let total_lines = map.buffer_snapshot.max_point().row + 1;
2474    let target_line = (count * total_lines as usize).div_ceil(100);
2475    let target_point = DisplayPoint::new(
2476        DisplayRow(target_line.saturating_sub(1) as u32),
2477        point.column(),
2478    );
2479    map.clip_point(target_point, Bias::Left)
2480}
2481
2482fn unmatched_forward(
2483    map: &DisplaySnapshot,
2484    mut display_point: DisplayPoint,
2485    char: char,
2486    times: usize,
2487) -> DisplayPoint {
2488    for _ in 0..times {
2489        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2490        let point = display_point.to_point(map);
2491        let offset = point.to_offset(&map.buffer_snapshot);
2492
2493        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2494        let Some(ranges) = ranges else { break };
2495        let mut closest_closing_destination = None;
2496        let mut closest_distance = usize::MAX;
2497
2498        for (_, close_range) in ranges {
2499            if close_range.start > offset {
2500                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2501                if Some(char) == chars.next() {
2502                    let distance = close_range.start - offset;
2503                    if distance < closest_distance {
2504                        closest_closing_destination = Some(close_range.start);
2505                        closest_distance = distance;
2506                        continue;
2507                    }
2508                }
2509            }
2510        }
2511
2512        let new_point = closest_closing_destination
2513            .map(|destination| destination.to_display_point(map))
2514            .unwrap_or(display_point);
2515        if new_point == display_point {
2516            break;
2517        }
2518        display_point = new_point;
2519    }
2520    return display_point;
2521}
2522
2523fn unmatched_backward(
2524    map: &DisplaySnapshot,
2525    mut display_point: DisplayPoint,
2526    char: char,
2527    times: usize,
2528) -> DisplayPoint {
2529    for _ in 0..times {
2530        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2531        let point = display_point.to_point(map);
2532        let offset = point.to_offset(&map.buffer_snapshot);
2533
2534        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2535        let Some(ranges) = ranges else {
2536            break;
2537        };
2538
2539        let mut closest_starting_destination = None;
2540        let mut closest_distance = usize::MAX;
2541
2542        for (start_range, _) in ranges {
2543            if start_range.start < offset {
2544                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2545                if Some(char) == chars.next() {
2546                    let distance = offset - start_range.start;
2547                    if distance < closest_distance {
2548                        closest_starting_destination = Some(start_range.start);
2549                        closest_distance = distance;
2550                        continue;
2551                    }
2552                }
2553            }
2554        }
2555
2556        let new_point = closest_starting_destination
2557            .map(|destination| destination.to_display_point(map))
2558            .unwrap_or(display_point);
2559        if new_point == display_point {
2560            break;
2561        } else {
2562            display_point = new_point;
2563        }
2564    }
2565    display_point
2566}
2567
2568fn find_forward(
2569    map: &DisplaySnapshot,
2570    from: DisplayPoint,
2571    before: bool,
2572    target: char,
2573    times: usize,
2574    mode: FindRange,
2575    smartcase: bool,
2576) -> Option<DisplayPoint> {
2577    let mut to = from;
2578    let mut found = false;
2579
2580    for _ in 0..times {
2581        found = false;
2582        let new_to = find_boundary(map, to, mode, |_, right| {
2583            found = is_character_match(target, right, smartcase);
2584            found
2585        });
2586        if to == new_to {
2587            break;
2588        }
2589        to = new_to;
2590    }
2591
2592    if found {
2593        if before && to.column() > 0 {
2594            *to.column_mut() -= 1;
2595            Some(map.clip_point(to, Bias::Left))
2596        } else if before && to.row().0 > 0 {
2597            *to.row_mut() -= 1;
2598            *to.column_mut() = map.line(to.row()).len() as u32;
2599            Some(map.clip_point(to, Bias::Left))
2600        } else {
2601            Some(to)
2602        }
2603    } else {
2604        None
2605    }
2606}
2607
2608fn find_backward(
2609    map: &DisplaySnapshot,
2610    from: DisplayPoint,
2611    after: bool,
2612    target: char,
2613    times: usize,
2614    mode: FindRange,
2615    smartcase: bool,
2616) -> DisplayPoint {
2617    let mut to = from;
2618
2619    for _ in 0..times {
2620        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2621            is_character_match(target, right, smartcase)
2622        });
2623        if to == new_to {
2624            break;
2625        }
2626        to = new_to;
2627    }
2628
2629    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2630    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2631        if after {
2632            *to.column_mut() += 1;
2633            map.clip_point(to, Bias::Right)
2634        } else {
2635            to
2636        }
2637    } else {
2638        from
2639    }
2640}
2641
2642fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2643    if smartcase {
2644        if target.is_uppercase() {
2645            target == other
2646        } else {
2647            target == other.to_ascii_lowercase()
2648        }
2649    } else {
2650        target == other
2651    }
2652}
2653
2654fn sneak(
2655    map: &DisplaySnapshot,
2656    from: DisplayPoint,
2657    first_target: char,
2658    second_target: char,
2659    times: usize,
2660    smartcase: bool,
2661) -> Option<DisplayPoint> {
2662    let mut to = from;
2663    let mut found = false;
2664
2665    for _ in 0..times {
2666        found = false;
2667        let new_to = find_boundary(
2668            map,
2669            movement::right(map, to),
2670            FindRange::MultiLine,
2671            |left, right| {
2672                found = is_character_match(first_target, left, smartcase)
2673                    && is_character_match(second_target, right, smartcase);
2674                found
2675            },
2676        );
2677        if to == new_to {
2678            break;
2679        }
2680        to = new_to;
2681    }
2682
2683    if found {
2684        Some(movement::left(map, to))
2685    } else {
2686        None
2687    }
2688}
2689
2690fn sneak_backward(
2691    map: &DisplaySnapshot,
2692    from: DisplayPoint,
2693    first_target: char,
2694    second_target: char,
2695    times: usize,
2696    smartcase: bool,
2697) -> Option<DisplayPoint> {
2698    let mut to = from;
2699    let mut found = false;
2700
2701    for _ in 0..times {
2702        found = false;
2703        let new_to =
2704            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2705                found = is_character_match(first_target, left, smartcase)
2706                    && is_character_match(second_target, right, smartcase);
2707                found
2708            });
2709        if to == new_to {
2710            break;
2711        }
2712        to = new_to;
2713    }
2714
2715    if found {
2716        Some(movement::left(map, to))
2717    } else {
2718        None
2719    }
2720}
2721
2722fn next_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 previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2728    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2729    first_non_whitespace(map, false, correct_line)
2730}
2731
2732fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2733    let correct_line = start_of_relative_buffer_row(map, point, 0);
2734    right(map, correct_line, times.saturating_sub(1))
2735}
2736
2737pub(crate) fn next_line_end(
2738    map: &DisplaySnapshot,
2739    mut point: DisplayPoint,
2740    times: usize,
2741) -> DisplayPoint {
2742    if times > 1 {
2743        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2744    }
2745    end_of_line(map, false, point, 1)
2746}
2747
2748fn window_top(
2749    map: &DisplaySnapshot,
2750    point: DisplayPoint,
2751    text_layout_details: &TextLayoutDetails,
2752    mut times: usize,
2753) -> (DisplayPoint, SelectionGoal) {
2754    let first_visible_line = text_layout_details
2755        .scroll_anchor
2756        .anchor
2757        .to_display_point(map);
2758
2759    if first_visible_line.row() != DisplayRow(0)
2760        && text_layout_details.vertical_scroll_margin as usize > times
2761    {
2762        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2763    }
2764
2765    if let Some(visible_rows) = text_layout_details.visible_rows {
2766        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2767        let new_row = (first_visible_line.row().0 + (times as u32))
2768            .min(bottom_row)
2769            .min(map.max_point().row().0);
2770        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2771
2772        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2773        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2774    } else {
2775        let new_row =
2776            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2777        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2778
2779        let new_point = DisplayPoint::new(new_row, new_col);
2780        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2781    }
2782}
2783
2784fn window_middle(
2785    map: &DisplaySnapshot,
2786    point: DisplayPoint,
2787    text_layout_details: &TextLayoutDetails,
2788) -> (DisplayPoint, SelectionGoal) {
2789    if let Some(visible_rows) = text_layout_details.visible_rows {
2790        let first_visible_line = text_layout_details
2791            .scroll_anchor
2792            .anchor
2793            .to_display_point(map);
2794
2795        let max_visible_rows =
2796            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2797
2798        let new_row =
2799            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2800        let new_row = DisplayRow(new_row);
2801        let new_col = point.column().min(map.line_len(new_row));
2802        let new_point = DisplayPoint::new(new_row, new_col);
2803        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2804    } else {
2805        (point, SelectionGoal::None)
2806    }
2807}
2808
2809fn window_bottom(
2810    map: &DisplaySnapshot,
2811    point: DisplayPoint,
2812    text_layout_details: &TextLayoutDetails,
2813    mut times: usize,
2814) -> (DisplayPoint, SelectionGoal) {
2815    if let Some(visible_rows) = text_layout_details.visible_rows {
2816        let first_visible_line = text_layout_details
2817            .scroll_anchor
2818            .anchor
2819            .to_display_point(map);
2820        let bottom_row = first_visible_line.row().0
2821            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2822        if bottom_row < map.max_point().row().0
2823            && text_layout_details.vertical_scroll_margin as usize > times
2824        {
2825            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2826        }
2827        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2828        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2829        {
2830            first_visible_line.row()
2831        } else {
2832            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2833        };
2834        let new_col = point.column().min(map.line_len(new_row));
2835        let new_point = DisplayPoint::new(new_row, new_col);
2836        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2837    } else {
2838        (point, SelectionGoal::None)
2839    }
2840}
2841
2842fn method_motion(
2843    map: &DisplaySnapshot,
2844    mut display_point: DisplayPoint,
2845    times: usize,
2846    direction: Direction,
2847    is_start: bool,
2848) -> DisplayPoint {
2849    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2850        return display_point;
2851    };
2852
2853    for _ in 0..times {
2854        let point = map.display_point_to_point(display_point, Bias::Left);
2855        let offset = point.to_offset(&map.buffer_snapshot);
2856        let range = if direction == Direction::Prev {
2857            0..offset
2858        } else {
2859            offset..buffer.len()
2860        };
2861
2862        let possibilities = buffer
2863            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2864            .filter_map(|(range, object)| {
2865                if !matches!(object, language::TextObject::AroundFunction) {
2866                    return None;
2867                }
2868
2869                let relevant = if is_start { range.start } else { range.end };
2870                if direction == Direction::Prev && relevant < offset {
2871                    Some(relevant)
2872                } else if direction == Direction::Next && relevant > offset + 1 {
2873                    Some(relevant)
2874                } else {
2875                    None
2876                }
2877            });
2878
2879        let dest = if direction == Direction::Prev {
2880            possibilities.max().unwrap_or(offset)
2881        } else {
2882            possibilities.min().unwrap_or(offset)
2883        };
2884        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2885        if new_point == display_point {
2886            break;
2887        }
2888        display_point = new_point;
2889    }
2890    display_point
2891}
2892
2893fn comment_motion(
2894    map: &DisplaySnapshot,
2895    mut display_point: DisplayPoint,
2896    times: usize,
2897    direction: Direction,
2898) -> DisplayPoint {
2899    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2900        return display_point;
2901    };
2902
2903    for _ in 0..times {
2904        let point = map.display_point_to_point(display_point, Bias::Left);
2905        let offset = point.to_offset(&map.buffer_snapshot);
2906        let range = if direction == Direction::Prev {
2907            0..offset
2908        } else {
2909            offset..buffer.len()
2910        };
2911
2912        let possibilities = buffer
2913            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2914            .filter_map(|(range, object)| {
2915                if !matches!(object, language::TextObject::AroundComment) {
2916                    return None;
2917                }
2918
2919                let relevant = if direction == Direction::Prev {
2920                    range.start
2921                } else {
2922                    range.end
2923                };
2924                if direction == Direction::Prev && relevant < offset {
2925                    Some(relevant)
2926                } else if direction == Direction::Next && relevant > offset + 1 {
2927                    Some(relevant)
2928                } else {
2929                    None
2930                }
2931            });
2932
2933        let dest = if direction == Direction::Prev {
2934            possibilities.max().unwrap_or(offset)
2935        } else {
2936            possibilities.min().unwrap_or(offset)
2937        };
2938        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2939        if new_point == display_point {
2940            break;
2941        }
2942        display_point = new_point;
2943    }
2944
2945    display_point
2946}
2947
2948fn section_motion(
2949    map: &DisplaySnapshot,
2950    mut display_point: DisplayPoint,
2951    times: usize,
2952    direction: Direction,
2953    is_start: bool,
2954) -> DisplayPoint {
2955    if map.buffer_snapshot.as_singleton().is_some() {
2956        for _ in 0..times {
2957            let offset = map
2958                .display_point_to_point(display_point, Bias::Left)
2959                .to_offset(&map.buffer_snapshot);
2960            let range = if direction == Direction::Prev {
2961                0..offset
2962            } else {
2963                offset..map.buffer_snapshot.len()
2964            };
2965
2966            // we set a max start depth here because we want a section to only be "top level"
2967            // similar to vim's default of '{' in the first column.
2968            // (and without it, ]] at the start of editor.rs is -very- slow)
2969            let mut possibilities = map
2970                .buffer_snapshot
2971                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2972                .filter(|(_, object)| {
2973                    matches!(
2974                        object,
2975                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2976                    )
2977                })
2978                .collect::<Vec<_>>();
2979            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2980            let mut prev_end = None;
2981            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2982                if t == language::TextObject::AroundFunction
2983                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2984                {
2985                    return None;
2986                }
2987                prev_end = Some(range.end);
2988
2989                let relevant = if is_start { range.start } else { range.end };
2990                if direction == Direction::Prev && relevant < offset {
2991                    Some(relevant)
2992                } else if direction == Direction::Next && relevant > offset + 1 {
2993                    Some(relevant)
2994                } else {
2995                    None
2996                }
2997            });
2998
2999            let offset = if direction == Direction::Prev {
3000                possibilities.max().unwrap_or(0)
3001            } else {
3002                possibilities.min().unwrap_or(map.buffer_snapshot.len())
3003            };
3004
3005            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
3006            if new_point == display_point {
3007                break;
3008            }
3009            display_point = new_point;
3010        }
3011        return display_point;
3012    };
3013
3014    for _ in 0..times {
3015        let next_point = if is_start {
3016            movement::start_of_excerpt(map, display_point, direction)
3017        } else {
3018            movement::end_of_excerpt(map, display_point, direction)
3019        };
3020        if next_point == display_point {
3021            break;
3022        }
3023        display_point = next_point;
3024    }
3025
3026    display_point
3027}
3028
3029fn matches_indent_type(
3030    target_indent: &text::LineIndent,
3031    current_indent: &text::LineIndent,
3032    indent_type: IndentType,
3033) -> bool {
3034    match indent_type {
3035        IndentType::Lesser => {
3036            target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3037        }
3038        IndentType::Greater => {
3039            target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3040        }
3041        IndentType::Same => {
3042            target_indent.spaces == current_indent.spaces
3043                && target_indent.tabs == current_indent.tabs
3044        }
3045    }
3046}
3047
3048fn indent_motion(
3049    map: &DisplaySnapshot,
3050    mut display_point: DisplayPoint,
3051    times: usize,
3052    direction: Direction,
3053    indent_type: IndentType,
3054) -> DisplayPoint {
3055    let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3056    let current_row = MultiBufferRow(buffer_point.row);
3057    let current_indent = map.line_indent_for_buffer_row(current_row);
3058    if current_indent.is_line_empty() {
3059        return display_point;
3060    }
3061    let max_row = map.max_point().to_point(map).row;
3062
3063    for _ in 0..times {
3064        let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3065
3066        let target_row = match direction {
3067            Direction::Next => (current_buffer_row + 1..=max_row).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            Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3073                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3074                !indent.is_line_empty()
3075                    && matches_indent_type(&indent, &current_indent, indent_type)
3076            }),
3077        }
3078        .unwrap_or(current_buffer_row);
3079
3080        let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3081        let new_point = first_non_whitespace(map, false, new_point);
3082        if new_point == display_point {
3083            break;
3084        }
3085        display_point = new_point;
3086    }
3087    display_point
3088}
3089
3090#[cfg(test)]
3091mod test {
3092
3093    use crate::{
3094        state::Mode,
3095        test::{NeovimBackedTestContext, VimTestContext},
3096    };
3097    use editor::display_map::Inlay;
3098    use indoc::indoc;
3099    use language::Point;
3100    use multi_buffer::MultiBufferRow;
3101
3102    #[gpui::test]
3103    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3104        let mut cx = NeovimBackedTestContext::new(cx).await;
3105
3106        let initial_state = indoc! {r"ˇabc
3107            def
3108
3109            paragraph
3110            the second
3111
3112
3113
3114            third and
3115            final"};
3116
3117        // goes down once
3118        cx.set_shared_state(initial_state).await;
3119        cx.simulate_shared_keystrokes("}").await;
3120        cx.shared_state().await.assert_eq(indoc! {r"abc
3121            def
3122            ˇ
3123            paragraph
3124            the second
3125
3126
3127
3128            third and
3129            final"});
3130
3131        // goes up once
3132        cx.simulate_shared_keystrokes("{").await;
3133        cx.shared_state().await.assert_eq(initial_state);
3134
3135        // goes down twice
3136        cx.simulate_shared_keystrokes("2 }").await;
3137        cx.shared_state().await.assert_eq(indoc! {r"abc
3138            def
3139
3140            paragraph
3141            the second
3142            ˇ
3143
3144
3145            third and
3146            final"});
3147
3148        // goes down over multiple blanks
3149        cx.simulate_shared_keystrokes("}").await;
3150        cx.shared_state().await.assert_eq(indoc! {r"abc
3151                def
3152
3153                paragraph
3154                the second
3155
3156
3157
3158                third and
3159                finaˇl"});
3160
3161        // goes up twice
3162        cx.simulate_shared_keystrokes("2 {").await;
3163        cx.shared_state().await.assert_eq(indoc! {r"abc
3164                def
3165                ˇ
3166                paragraph
3167                the second
3168
3169
3170
3171                third and
3172                final"});
3173    }
3174
3175    #[gpui::test]
3176    async fn test_matching(cx: &mut gpui::TestAppContext) {
3177        let mut cx = NeovimBackedTestContext::new(cx).await;
3178
3179        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3180                do(something(with<Types>.and_arrays[0, 2]))
3181            }"})
3182            .await;
3183        cx.simulate_shared_keystrokes("%").await;
3184        cx.shared_state()
3185            .await
3186            .assert_eq(indoc! {r"func (a stringˇ) {
3187                do(something(with<Types>.and_arrays[0, 2]))
3188            }"});
3189
3190        // test it works on the last character of the line
3191        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3192            do(something(with<Types>.and_arrays[0, 2]))
3193            }"})
3194            .await;
3195        cx.simulate_shared_keystrokes("%").await;
3196        cx.shared_state()
3197            .await
3198            .assert_eq(indoc! {r"func (a string) {
3199            do(something(with<Types>.and_arrays[0, 2]))
3200            ˇ}"});
3201
3202        // test it works on immediate nesting
3203        cx.set_shared_state("ˇ{()}").await;
3204        cx.simulate_shared_keystrokes("%").await;
3205        cx.shared_state().await.assert_eq("{()ˇ}");
3206        cx.simulate_shared_keystrokes("%").await;
3207        cx.shared_state().await.assert_eq("ˇ{()}");
3208
3209        // test it works on immediate nesting inside braces
3210        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3211        cx.simulate_shared_keystrokes("%").await;
3212        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3213
3214        // test it jumps to the next paren on a line
3215        cx.set_shared_state("func ˇboop() {\n}").await;
3216        cx.simulate_shared_keystrokes("%").await;
3217        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3218    }
3219
3220    #[gpui::test]
3221    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3222        let mut cx = NeovimBackedTestContext::new(cx).await;
3223
3224        // test it works with curly braces
3225        cx.set_shared_state(indoc! {r"func (a string) {
3226                do(something(with<Types>.anˇd_arrays[0, 2]))
3227            }"})
3228            .await;
3229        cx.simulate_shared_keystrokes("] }").await;
3230        cx.shared_state()
3231            .await
3232            .assert_eq(indoc! {r"func (a string) {
3233                do(something(with<Types>.and_arrays[0, 2]))
3234            ˇ}"});
3235
3236        // test it works with brackets
3237        cx.set_shared_state(indoc! {r"func (a string) {
3238                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3239            }"})
3240            .await;
3241        cx.simulate_shared_keystrokes("] )").await;
3242        cx.shared_state()
3243            .await
3244            .assert_eq(indoc! {r"func (a string) {
3245                do(something(with<Types>.and_arrays[0, 2])ˇ)
3246            }"});
3247
3248        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3249            .await;
3250        cx.simulate_shared_keystrokes("] )").await;
3251        cx.shared_state()
3252            .await
3253            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3254
3255        // test it works on immediate nesting
3256        cx.set_shared_state("{ˇ {}{}}").await;
3257        cx.simulate_shared_keystrokes("] }").await;
3258        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3259        cx.set_shared_state("(ˇ ()())").await;
3260        cx.simulate_shared_keystrokes("] )").await;
3261        cx.shared_state().await.assert_eq("( ()()ˇ)");
3262
3263        // test it works on immediate nesting inside braces
3264        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3265        cx.simulate_shared_keystrokes("] }").await;
3266        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3267        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3268        cx.simulate_shared_keystrokes("] )").await;
3269        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3270    }
3271
3272    #[gpui::test]
3273    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3274        let mut cx = NeovimBackedTestContext::new(cx).await;
3275
3276        // test it works with curly braces
3277        cx.set_shared_state(indoc! {r"func (a string) {
3278                do(something(with<Types>.anˇd_arrays[0, 2]))
3279            }"})
3280            .await;
3281        cx.simulate_shared_keystrokes("[ {").await;
3282        cx.shared_state()
3283            .await
3284            .assert_eq(indoc! {r"func (a string) ˇ{
3285                do(something(with<Types>.and_arrays[0, 2]))
3286            }"});
3287
3288        // test it works with brackets
3289        cx.set_shared_state(indoc! {r"func (a string) {
3290                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3291            }"})
3292            .await;
3293        cx.simulate_shared_keystrokes("[ (").await;
3294        cx.shared_state()
3295            .await
3296            .assert_eq(indoc! {r"func (a string) {
3297                doˇ(something(with<Types>.and_arrays[0, 2]))
3298            }"});
3299
3300        // test it works on immediate nesting
3301        cx.set_shared_state("{{}{} ˇ }").await;
3302        cx.simulate_shared_keystrokes("[ {").await;
3303        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3304        cx.set_shared_state("(()() ˇ )").await;
3305        cx.simulate_shared_keystrokes("[ (").await;
3306        cx.shared_state().await.assert_eq("ˇ(()()  )");
3307
3308        // test it works on immediate nesting inside braces
3309        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3310        cx.simulate_shared_keystrokes("[ {").await;
3311        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3312        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3313        cx.simulate_shared_keystrokes("[ (").await;
3314        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3315    }
3316
3317    #[gpui::test]
3318    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3319        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3320
3321        cx.neovim.exec("set filetype=html").await;
3322
3323        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3324        cx.simulate_shared_keystrokes("%").await;
3325        cx.shared_state()
3326            .await
3327            .assert_eq(indoc! {r"<body><ˇ/body>"});
3328        cx.simulate_shared_keystrokes("%").await;
3329
3330        // test jumping backwards
3331        cx.shared_state()
3332            .await
3333            .assert_eq(indoc! {r"<ˇbody></body>"});
3334
3335        // test self-closing tags
3336        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3337        cx.simulate_shared_keystrokes("%").await;
3338        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3339
3340        // test tag with attributes
3341        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3342            </div>
3343            "})
3344            .await;
3345        cx.simulate_shared_keystrokes("%").await;
3346        cx.shared_state()
3347            .await
3348            .assert_eq(indoc! {r"<div class='test' id='main'>
3349            <ˇ/div>
3350            "});
3351
3352        // test multi-line self-closing tag
3353        cx.set_shared_state(indoc! {r#"<a>
3354            <br
3355                test = "test"
3356            /ˇ>
3357        </a>"#})
3358            .await;
3359        cx.simulate_shared_keystrokes("%").await;
3360        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3361            ˇ<br
3362                test = "test"
3363            />
3364        </a>"#});
3365    }
3366
3367    #[gpui::test]
3368    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3369        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3370
3371        // test brackets within tags
3372        cx.set_shared_state(indoc! {r"function f() {
3373            return (
3374                <div rules={ˇ[{ a: 1 }]}>
3375                    <h1>test</h1>
3376                </div>
3377            );
3378        }"})
3379            .await;
3380        cx.simulate_shared_keystrokes("%").await;
3381        cx.shared_state().await.assert_eq(indoc! {r"function f() {
3382            return (
3383                <div rules={[{ a: 1 }ˇ]}>
3384                    <h1>test</h1>
3385                </div>
3386            );
3387        }"});
3388    }
3389
3390    #[gpui::test]
3391    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3392        let mut cx = NeovimBackedTestContext::new(cx).await;
3393
3394        // f and F
3395        cx.set_shared_state("ˇone two three four").await;
3396        cx.simulate_shared_keystrokes("f o").await;
3397        cx.shared_state().await.assert_eq("one twˇo three four");
3398        cx.simulate_shared_keystrokes(",").await;
3399        cx.shared_state().await.assert_eq("ˇone two three four");
3400        cx.simulate_shared_keystrokes("2 ;").await;
3401        cx.shared_state().await.assert_eq("one two three fˇour");
3402        cx.simulate_shared_keystrokes("shift-f e").await;
3403        cx.shared_state().await.assert_eq("one two threˇe four");
3404        cx.simulate_shared_keystrokes("2 ;").await;
3405        cx.shared_state().await.assert_eq("onˇe two three four");
3406        cx.simulate_shared_keystrokes(",").await;
3407        cx.shared_state().await.assert_eq("one two thrˇee four");
3408
3409        // t and T
3410        cx.set_shared_state("ˇone two three four").await;
3411        cx.simulate_shared_keystrokes("t o").await;
3412        cx.shared_state().await.assert_eq("one tˇwo three four");
3413        cx.simulate_shared_keystrokes(",").await;
3414        cx.shared_state().await.assert_eq("oˇne two three four");
3415        cx.simulate_shared_keystrokes("2 ;").await;
3416        cx.shared_state().await.assert_eq("one two three ˇfour");
3417        cx.simulate_shared_keystrokes("shift-t e").await;
3418        cx.shared_state().await.assert_eq("one two threeˇ four");
3419        cx.simulate_shared_keystrokes("3 ;").await;
3420        cx.shared_state().await.assert_eq("oneˇ two three four");
3421        cx.simulate_shared_keystrokes(",").await;
3422        cx.shared_state().await.assert_eq("one two thˇree four");
3423    }
3424
3425    #[gpui::test]
3426    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3427        let mut cx = NeovimBackedTestContext::new(cx).await;
3428        let initial_state = indoc! {r"something(ˇfoo)"};
3429        cx.set_shared_state(initial_state).await;
3430        cx.simulate_shared_keystrokes("}").await;
3431        cx.shared_state().await.assert_eq("something(fooˇ)");
3432    }
3433
3434    #[gpui::test]
3435    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3436        let mut cx = NeovimBackedTestContext::new(cx).await;
3437        cx.set_shared_state("ˇone\n  two\nthree").await;
3438        cx.simulate_shared_keystrokes("enter").await;
3439        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3440    }
3441
3442    #[gpui::test]
3443    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3444        let mut cx = NeovimBackedTestContext::new(cx).await;
3445        cx.set_shared_state("ˇ one\n two \nthree").await;
3446        cx.simulate_shared_keystrokes("g _").await;
3447        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3448
3449        cx.set_shared_state("ˇ one \n two \nthree").await;
3450        cx.simulate_shared_keystrokes("g _").await;
3451        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3452        cx.simulate_shared_keystrokes("2 g _").await;
3453        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3454    }
3455
3456    #[gpui::test]
3457    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3458        let mut cx = NeovimBackedTestContext::new(cx).await;
3459        let initial_state = indoc! {r"abc
3460          def
3461          paragraph
3462          the second
3463          third ˇand
3464          final"};
3465
3466        cx.set_shared_state(initial_state).await;
3467        cx.simulate_shared_keystrokes("shift-h").await;
3468        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3469          def
3470          paragraph
3471          the second
3472          third and
3473          final"});
3474
3475        // clip point
3476        cx.set_shared_state(indoc! {r"
3477          1 2 3
3478          4 5 6
3479          7 8 ˇ9
3480          "})
3481            .await;
3482        cx.simulate_shared_keystrokes("shift-h").await;
3483        cx.shared_state().await.assert_eq(indoc! {"
3484          1 2 ˇ3
3485          4 5 6
3486          7 8 9
3487          "});
3488
3489        cx.set_shared_state(indoc! {r"
3490          1 2 3
3491          4 5 6
3492          ˇ7 8 9
3493          "})
3494            .await;
3495        cx.simulate_shared_keystrokes("shift-h").await;
3496        cx.shared_state().await.assert_eq(indoc! {"
3497          ˇ1 2 3
3498          4 5 6
3499          7 8 9
3500          "});
3501
3502        cx.set_shared_state(indoc! {r"
3503          1 2 3
3504          4 5 ˇ6
3505          7 8 9"})
3506            .await;
3507        cx.simulate_shared_keystrokes("9 shift-h").await;
3508        cx.shared_state().await.assert_eq(indoc! {"
3509          1 2 3
3510          4 5 6
3511          7 8 ˇ9"});
3512    }
3513
3514    #[gpui::test]
3515    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3516        let mut cx = NeovimBackedTestContext::new(cx).await;
3517        let initial_state = indoc! {r"abˇc
3518          def
3519          paragraph
3520          the second
3521          third and
3522          final"};
3523
3524        cx.set_shared_state(initial_state).await;
3525        cx.simulate_shared_keystrokes("shift-m").await;
3526        cx.shared_state().await.assert_eq(indoc! {r"abc
3527          def
3528          paˇragraph
3529          the second
3530          third and
3531          final"});
3532
3533        cx.set_shared_state(indoc! {r"
3534          1 2 3
3535          4 5 6
3536          7 8 ˇ9
3537          "})
3538            .await;
3539        cx.simulate_shared_keystrokes("shift-m").await;
3540        cx.shared_state().await.assert_eq(indoc! {"
3541          1 2 3
3542          4 5 ˇ6
3543          7 8 9
3544          "});
3545        cx.set_shared_state(indoc! {r"
3546          1 2 3
3547          4 5 6
3548          ˇ7 8 9
3549          "})
3550            .await;
3551        cx.simulate_shared_keystrokes("shift-m").await;
3552        cx.shared_state().await.assert_eq(indoc! {"
3553          1 2 3
3554          ˇ4 5 6
3555          7 8 9
3556          "});
3557        cx.set_shared_state(indoc! {r"
3558          ˇ1 2 3
3559          4 5 6
3560          7 8 9
3561          "})
3562            .await;
3563        cx.simulate_shared_keystrokes("shift-m").await;
3564        cx.shared_state().await.assert_eq(indoc! {"
3565          1 2 3
3566          ˇ4 5 6
3567          7 8 9
3568          "});
3569        cx.set_shared_state(indoc! {r"
3570          1 2 3
3571          ˇ4 5 6
3572          7 8 9
3573          "})
3574            .await;
3575        cx.simulate_shared_keystrokes("shift-m").await;
3576        cx.shared_state().await.assert_eq(indoc! {"
3577          1 2 3
3578          ˇ4 5 6
3579          7 8 9
3580          "});
3581        cx.set_shared_state(indoc! {r"
3582          1 2 3
3583          4 5 ˇ6
3584          7 8 9
3585          "})
3586            .await;
3587        cx.simulate_shared_keystrokes("shift-m").await;
3588        cx.shared_state().await.assert_eq(indoc! {"
3589          1 2 3
3590          4 5 ˇ6
3591          7 8 9
3592          "});
3593    }
3594
3595    #[gpui::test]
3596    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3597        let mut cx = NeovimBackedTestContext::new(cx).await;
3598        let initial_state = indoc! {r"abc
3599          deˇf
3600          paragraph
3601          the second
3602          third and
3603          final"};
3604
3605        cx.set_shared_state(initial_state).await;
3606        cx.simulate_shared_keystrokes("shift-l").await;
3607        cx.shared_state().await.assert_eq(indoc! {r"abc
3608          def
3609          paragraph
3610          the second
3611          third and
3612          fiˇnal"});
3613
3614        cx.set_shared_state(indoc! {r"
3615          1 2 3
3616          4 5 ˇ6
3617          7 8 9
3618          "})
3619            .await;
3620        cx.simulate_shared_keystrokes("shift-l").await;
3621        cx.shared_state().await.assert_eq(indoc! {"
3622          1 2 3
3623          4 5 6
3624          7 8 9
3625          ˇ"});
3626
3627        cx.set_shared_state(indoc! {r"
3628          1 2 3
3629          ˇ4 5 6
3630          7 8 9
3631          "})
3632            .await;
3633        cx.simulate_shared_keystrokes("shift-l").await;
3634        cx.shared_state().await.assert_eq(indoc! {"
3635          1 2 3
3636          4 5 6
3637          7 8 9
3638          ˇ"});
3639
3640        cx.set_shared_state(indoc! {r"
3641          1 2 ˇ3
3642          4 5 6
3643          7 8 9
3644          "})
3645            .await;
3646        cx.simulate_shared_keystrokes("shift-l").await;
3647        cx.shared_state().await.assert_eq(indoc! {"
3648          1 2 3
3649          4 5 6
3650          7 8 9
3651          ˇ"});
3652
3653        cx.set_shared_state(indoc! {r"
3654          ˇ1 2 3
3655          4 5 6
3656          7 8 9
3657          "})
3658            .await;
3659        cx.simulate_shared_keystrokes("shift-l").await;
3660        cx.shared_state().await.assert_eq(indoc! {"
3661          1 2 3
3662          4 5 6
3663          7 8 9
3664          ˇ"});
3665
3666        cx.set_shared_state(indoc! {r"
3667          1 2 3
3668          4 5 ˇ6
3669          7 8 9
3670          "})
3671            .await;
3672        cx.simulate_shared_keystrokes("9 shift-l").await;
3673        cx.shared_state().await.assert_eq(indoc! {"
3674          1 2 ˇ3
3675          4 5 6
3676          7 8 9
3677          "});
3678    }
3679
3680    #[gpui::test]
3681    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3682        let mut cx = NeovimBackedTestContext::new(cx).await;
3683        cx.set_shared_state(indoc! {r"
3684        456 5ˇ67 678
3685        "})
3686            .await;
3687        cx.simulate_shared_keystrokes("g e").await;
3688        cx.shared_state().await.assert_eq(indoc! {"
3689        45ˇ6 567 678
3690        "});
3691
3692        // Test times
3693        cx.set_shared_state(indoc! {r"
3694        123 234 345
3695        456 5ˇ67 678
3696        "})
3697            .await;
3698        cx.simulate_shared_keystrokes("4 g e").await;
3699        cx.shared_state().await.assert_eq(indoc! {"
3700        12ˇ3 234 345
3701        456 567 678
3702        "});
3703
3704        // With punctuation
3705        cx.set_shared_state(indoc! {r"
3706        123 234 345
3707        4;5.6 5ˇ67 678
3708        789 890 901
3709        "})
3710            .await;
3711        cx.simulate_shared_keystrokes("g e").await;
3712        cx.shared_state().await.assert_eq(indoc! {"
3713          123 234 345
3714          4;5.ˇ6 567 678
3715          789 890 901
3716        "});
3717
3718        // With punctuation and count
3719        cx.set_shared_state(indoc! {r"
3720        123 234 345
3721        4;5.6 5ˇ67 678
3722        789 890 901
3723        "})
3724            .await;
3725        cx.simulate_shared_keystrokes("5 g e").await;
3726        cx.shared_state().await.assert_eq(indoc! {"
3727          123 234 345
3728          ˇ4;5.6 567 678
3729          789 890 901
3730        "});
3731
3732        // newlines
3733        cx.set_shared_state(indoc! {r"
3734        123 234 345
3735
3736        78ˇ9 890 901
3737        "})
3738            .await;
3739        cx.simulate_shared_keystrokes("g e").await;
3740        cx.shared_state().await.assert_eq(indoc! {"
3741          123 234 345
3742          ˇ
3743          789 890 901
3744        "});
3745        cx.simulate_shared_keystrokes("g e").await;
3746        cx.shared_state().await.assert_eq(indoc! {"
3747          123 234 34ˇ5
3748
3749          789 890 901
3750        "});
3751
3752        // With punctuation
3753        cx.set_shared_state(indoc! {r"
3754        123 234 345
3755        4;5.ˇ6 567 678
3756        789 890 901
3757        "})
3758            .await;
3759        cx.simulate_shared_keystrokes("g shift-e").await;
3760        cx.shared_state().await.assert_eq(indoc! {"
3761          123 234 34ˇ5
3762          4;5.6 567 678
3763          789 890 901
3764        "});
3765
3766        // With multi byte char
3767        cx.set_shared_state(indoc! {r"
3768        bar ˇó
3769        "})
3770            .await;
3771        cx.simulate_shared_keystrokes("g e").await;
3772        cx.shared_state().await.assert_eq(indoc! {"
3773        baˇr ó
3774        "});
3775    }
3776
3777    #[gpui::test]
3778    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3779        let mut cx = NeovimBackedTestContext::new(cx).await;
3780
3781        cx.set_shared_state(indoc! {"
3782            fn aˇ() {
3783              return
3784            }
3785        "})
3786            .await;
3787        cx.simulate_shared_keystrokes("v $ %").await;
3788        cx.shared_state().await.assert_eq(indoc! {"
3789            fn a«() {
3790              return
3791            }ˇ»
3792        "});
3793    }
3794
3795    #[gpui::test]
3796    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3797        let mut cx = VimTestContext::new(cx, true).await;
3798
3799        cx.set_state(
3800            indoc! {"
3801                struct Foo {
3802                ˇ
3803                }
3804            "},
3805            Mode::Normal,
3806        );
3807
3808        cx.update_editor(|editor, _window, cx| {
3809            let range = editor.selections.newest_anchor().range();
3810            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3811            let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
3812            editor.splice_inlays(&[], vec![inlay], cx);
3813        });
3814
3815        cx.simulate_keystrokes("j");
3816        cx.assert_state(
3817            indoc! {"
3818                struct Foo {
3819
3820                ˇ}
3821            "},
3822            Mode::Normal,
3823        );
3824    }
3825
3826    #[gpui::test]
3827    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3828        let mut cx = VimTestContext::new(cx, true).await;
3829
3830        cx.set_state(
3831            indoc! {"
3832            ˇstruct Foo {
3833
3834            }
3835        "},
3836            Mode::Normal,
3837        );
3838        cx.update_editor(|editor, _window, cx| {
3839            let snapshot = editor.buffer().read(cx).snapshot(cx);
3840            let end_of_line =
3841                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3842            let inlay_text = " hint";
3843            let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
3844            editor.splice_inlays(&[], vec![inlay], cx);
3845        });
3846        cx.simulate_keystrokes("$");
3847        cx.assert_state(
3848            indoc! {"
3849            struct Foo ˇ{
3850
3851            }
3852        "},
3853            Mode::Normal,
3854        );
3855    }
3856
3857    #[gpui::test]
3858    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3859        let mut cx = NeovimBackedTestContext::new(cx).await;
3860        // Normal mode
3861        cx.set_shared_state(indoc! {"
3862            The ˇquick brown
3863            fox jumps over
3864            the lazy dog
3865            The quick brown
3866            fox jumps over
3867            the lazy dog
3868            The quick brown
3869            fox jumps over
3870            the lazy dog"})
3871            .await;
3872        cx.simulate_shared_keystrokes("2 0 %").await;
3873        cx.shared_state().await.assert_eq(indoc! {"
3874            The quick brown
3875            fox ˇjumps over
3876            the lazy dog
3877            The quick brown
3878            fox jumps over
3879            the lazy dog
3880            The quick brown
3881            fox jumps over
3882            the lazy dog"});
3883
3884        cx.simulate_shared_keystrokes("2 5 %").await;
3885        cx.shared_state().await.assert_eq(indoc! {"
3886            The quick brown
3887            fox jumps over
3888            the ˇlazy dog
3889            The quick brown
3890            fox jumps over
3891            the lazy dog
3892            The quick brown
3893            fox jumps over
3894            the lazy dog"});
3895
3896        cx.simulate_shared_keystrokes("7 5 %").await;
3897        cx.shared_state().await.assert_eq(indoc! {"
3898            The quick brown
3899            fox jumps over
3900            the lazy dog
3901            The quick brown
3902            fox jumps over
3903            the lazy dog
3904            The ˇquick brown
3905            fox jumps over
3906            the lazy dog"});
3907
3908        // Visual mode
3909        cx.set_shared_state(indoc! {"
3910            The ˇquick brown
3911            fox jumps over
3912            the lazy dog
3913            The quick brown
3914            fox jumps over
3915            the lazy dog
3916            The quick brown
3917            fox jumps over
3918            the lazy dog"})
3919            .await;
3920        cx.simulate_shared_keystrokes("v 5 0 %").await;
3921        cx.shared_state().await.assert_eq(indoc! {"
3922            The «quick brown
3923            fox jumps over
3924            the lazy dog
3925            The quick brown
3926            fox jˇ»umps over
3927            the lazy dog
3928            The quick brown
3929            fox jumps over
3930            the lazy dog"});
3931
3932        cx.set_shared_state(indoc! {"
3933            The ˇquick brown
3934            fox jumps over
3935            the lazy dog
3936            The quick brown
3937            fox jumps over
3938            the lazy dog
3939            The quick brown
3940            fox jumps over
3941            the lazy dog"})
3942            .await;
3943        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3944        cx.shared_state().await.assert_eq(indoc! {"
3945            The «quick brown
3946            fox jumps over
3947            the lazy dog
3948            The quick brown
3949            fox jumps over
3950            the lazy dog
3951            The quick brown
3952            fox jumps over
3953            the lˇ»azy dog"});
3954    }
3955
3956    #[gpui::test]
3957    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3958        let mut cx = NeovimBackedTestContext::new(cx).await;
3959
3960        cx.set_shared_state("ˇπππππ").await;
3961        cx.simulate_shared_keystrokes("3 space").await;
3962        cx.shared_state().await.assert_eq("πππˇππ");
3963    }
3964
3965    #[gpui::test]
3966    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3967        let mut cx = NeovimBackedTestContext::new(cx).await;
3968
3969        cx.set_shared_state(indoc! {"
3970            ππππˇπ
3971            πanotherline"})
3972            .await;
3973        cx.simulate_shared_keystrokes("4 space").await;
3974        cx.shared_state().await.assert_eq(indoc! {"
3975            πππππ
3976            πanˇotherline"});
3977    }
3978
3979    #[gpui::test]
3980    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3981        let mut cx = NeovimBackedTestContext::new(cx).await;
3982
3983        cx.set_shared_state(indoc! {"
3984                        ππππ
3985                        πanˇotherline"})
3986            .await;
3987        cx.simulate_shared_keystrokes("4 backspace").await;
3988        cx.shared_state().await.assert_eq(indoc! {"
3989                        πππˇπ
3990                        πanotherline"});
3991    }
3992
3993    #[gpui::test]
3994    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3995        let mut cx = VimTestContext::new(cx, true).await;
3996        cx.set_state(
3997            indoc! {
3998                "func empty(a string) bool {
3999                     ˇif a == \"\" {
4000                         return true
4001                     }
4002                     return false
4003                }"
4004            },
4005            Mode::Normal,
4006        );
4007        cx.simulate_keystrokes("[ -");
4008        cx.assert_state(
4009            indoc! {
4010                "ˇfunc empty(a string) bool {
4011                     if a == \"\" {
4012                         return true
4013                     }
4014                     return false
4015                }"
4016            },
4017            Mode::Normal,
4018        );
4019        cx.simulate_keystrokes("] =");
4020        cx.assert_state(
4021            indoc! {
4022                "func empty(a string) bool {
4023                     if a == \"\" {
4024                         return true
4025                     }
4026                     return false
4027                ˇ}"
4028            },
4029            Mode::Normal,
4030        );
4031        cx.simulate_keystrokes("[ +");
4032        cx.assert_state(
4033            indoc! {
4034                "func empty(a string) bool {
4035                     if a == \"\" {
4036                         return true
4037                     }
4038                     ˇreturn false
4039                }"
4040            },
4041            Mode::Normal,
4042        );
4043        cx.simulate_keystrokes("2 [ =");
4044        cx.assert_state(
4045            indoc! {
4046                "func empty(a string) bool {
4047                     ˇif a == \"\" {
4048                         return true
4049                     }
4050                     return false
4051                }"
4052            },
4053            Mode::Normal,
4054        );
4055        cx.simulate_keystrokes("] +");
4056        cx.assert_state(
4057            indoc! {
4058                "func empty(a string) bool {
4059                     if a == \"\" {
4060                         ˇreturn true
4061                     }
4062                     return false
4063                }"
4064            },
4065            Mode::Normal,
4066        );
4067        cx.simulate_keystrokes("] -");
4068        cx.assert_state(
4069            indoc! {
4070                "func empty(a string) bool {
4071                     if a == \"\" {
4072                         return true
4073                     ˇ}
4074                     return false
4075                }"
4076            },
4077            Mode::Normal,
4078        );
4079    }
4080
4081    #[gpui::test]
4082    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4083        let mut cx = NeovimBackedTestContext::new(cx).await;
4084        cx.set_shared_state("abˇc").await;
4085        cx.simulate_shared_keystrokes("delete").await;
4086        cx.shared_state().await.assert_eq("aˇb");
4087    }
4088
4089    #[gpui::test]
4090    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4091        let mut cx = NeovimBackedTestContext::new(cx).await;
4092
4093        cx.set_shared_state(indoc! {"
4094             ˇthe quick brown fox
4095             jumped over the lazy dog"})
4096            .await;
4097        cx.simulate_shared_keystrokes("d v 0").await;
4098        cx.shared_state().await.assert_eq(indoc! {"
4099             ˇhe quick brown fox
4100             jumped over the lazy dog"});
4101        assert_eq!(cx.cx.forced_motion(), false);
4102
4103        cx.set_shared_state(indoc! {"
4104            the quick bˇrown fox
4105            jumped over the lazy dog"})
4106            .await;
4107        cx.simulate_shared_keystrokes("d v 0").await;
4108        cx.shared_state().await.assert_eq(indoc! {"
4109            ˇown fox
4110            jumped over the lazy dog"});
4111        assert_eq!(cx.cx.forced_motion(), false);
4112
4113        cx.set_shared_state(indoc! {"
4114            the quick brown foˇx
4115            jumped over the lazy dog"})
4116            .await;
4117        cx.simulate_shared_keystrokes("d v 0").await;
4118        cx.shared_state().await.assert_eq(indoc! {"
4119            ˇ
4120            jumped over the lazy dog"});
4121        assert_eq!(cx.cx.forced_motion(), false);
4122    }
4123
4124    #[gpui::test]
4125    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4126        let mut cx = NeovimBackedTestContext::new(cx).await;
4127
4128        cx.set_shared_state(indoc! {"
4129             ˇthe quick brown fox
4130             jumped over the lazy dog"})
4131            .await;
4132        cx.simulate_shared_keystrokes("d v g shift-m").await;
4133        cx.shared_state().await.assert_eq(indoc! {"
4134             ˇbrown fox
4135             jumped over the lazy dog"});
4136        assert_eq!(cx.cx.forced_motion(), false);
4137
4138        cx.set_shared_state(indoc! {"
4139            the quick bˇrown fox
4140            jumped over the lazy dog"})
4141            .await;
4142        cx.simulate_shared_keystrokes("d v g shift-m").await;
4143        cx.shared_state().await.assert_eq(indoc! {"
4144            the quickˇown fox
4145            jumped over the lazy dog"});
4146        assert_eq!(cx.cx.forced_motion(), false);
4147
4148        cx.set_shared_state(indoc! {"
4149            the quick brown foˇx
4150            jumped over the lazy dog"})
4151            .await;
4152        cx.simulate_shared_keystrokes("d v g shift-m").await;
4153        cx.shared_state().await.assert_eq(indoc! {"
4154            the quicˇk
4155            jumped over the lazy dog"});
4156        assert_eq!(cx.cx.forced_motion(), false);
4157
4158        cx.set_shared_state(indoc! {"
4159            ˇthe quick brown fox
4160            jumped over the lazy dog"})
4161            .await;
4162        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4163        cx.shared_state().await.assert_eq(indoc! {"
4164            ˇ fox
4165            jumped over the lazy dog"});
4166        assert_eq!(cx.cx.forced_motion(), false);
4167
4168        cx.set_shared_state(indoc! {"
4169            ˇthe quick brown fox
4170            jumped over the lazy dog"})
4171            .await;
4172        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4173        cx.shared_state().await.assert_eq(indoc! {"
4174            ˇuick brown fox
4175            jumped over the lazy dog"});
4176        assert_eq!(cx.cx.forced_motion(), false);
4177    }
4178
4179    #[gpui::test]
4180    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4181        let mut cx = NeovimBackedTestContext::new(cx).await;
4182
4183        cx.set_shared_state(indoc! {"
4184             the quick brown foˇx
4185             jumped over the lazy dog"})
4186            .await;
4187        cx.simulate_shared_keystrokes("d v $").await;
4188        cx.shared_state().await.assert_eq(indoc! {"
4189             the quick brown foˇx
4190             jumped over the lazy dog"});
4191        assert_eq!(cx.cx.forced_motion(), false);
4192
4193        cx.set_shared_state(indoc! {"
4194             ˇthe quick brown fox
4195             jumped over the lazy dog"})
4196            .await;
4197        cx.simulate_shared_keystrokes("d v $").await;
4198        cx.shared_state().await.assert_eq(indoc! {"
4199             ˇx
4200             jumped over the lazy dog"});
4201        assert_eq!(cx.cx.forced_motion(), false);
4202    }
4203
4204    #[gpui::test]
4205    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4206        let mut cx = NeovimBackedTestContext::new(cx).await;
4207
4208        cx.set_shared_state(indoc! {"
4209               ˇthe quick brown fox
4210               jumped over the lazy dog"})
4211            .await;
4212        cx.simulate_shared_keystrokes("y v j p").await;
4213        cx.shared_state().await.assert_eq(indoc! {"
4214               the quick brown fox
4215               ˇthe quick brown fox
4216               jumped over the lazy dog"});
4217        assert_eq!(cx.cx.forced_motion(), false);
4218
4219        cx.set_shared_state(indoc! {"
4220              the quick bˇrown fox
4221              jumped over the lazy dog"})
4222            .await;
4223        cx.simulate_shared_keystrokes("y v j p").await;
4224        cx.shared_state().await.assert_eq(indoc! {"
4225              the quick brˇrown fox
4226              jumped overown fox
4227              jumped over the lazy dog"});
4228        assert_eq!(cx.cx.forced_motion(), false);
4229
4230        cx.set_shared_state(indoc! {"
4231             the quick brown foˇx
4232             jumped over the lazy dog"})
4233            .await;
4234        cx.simulate_shared_keystrokes("y v j p").await;
4235        cx.shared_state().await.assert_eq(indoc! {"
4236             the quick brown foxˇx
4237             jumped over the la
4238             jumped over the lazy dog"});
4239        assert_eq!(cx.cx.forced_motion(), false);
4240
4241        cx.set_shared_state(indoc! {"
4242             the quick brown fox
4243             jˇumped over the lazy dog"})
4244            .await;
4245        cx.simulate_shared_keystrokes("y v k p").await;
4246        cx.shared_state().await.assert_eq(indoc! {"
4247            thˇhe quick brown fox
4248            je quick brown fox
4249            jumped over the lazy dog"});
4250        assert_eq!(cx.cx.forced_motion(), false);
4251    }
4252
4253    #[gpui::test]
4254    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4255        let mut cx = NeovimBackedTestContext::new(cx).await;
4256
4257        cx.set_shared_state(indoc! {"
4258              ˇthe quick brown fox
4259              jumped over the lazy dog"})
4260            .await;
4261        cx.simulate_shared_keystrokes("d v e").await;
4262        cx.shared_state().await.assert_eq(indoc! {"
4263              ˇe quick brown fox
4264              jumped over the lazy dog"});
4265        assert_eq!(cx.cx.forced_motion(), false);
4266
4267        cx.set_shared_state(indoc! {"
4268              the quick bˇrown fox
4269              jumped over the lazy dog"})
4270            .await;
4271        cx.simulate_shared_keystrokes("d v e").await;
4272        cx.shared_state().await.assert_eq(indoc! {"
4273              the quick bˇn fox
4274              jumped over the lazy dog"});
4275        assert_eq!(cx.cx.forced_motion(), false);
4276
4277        cx.set_shared_state(indoc! {"
4278             the quick brown foˇx
4279             jumped over the lazy dog"})
4280            .await;
4281        cx.simulate_shared_keystrokes("d v e").await;
4282        cx.shared_state().await.assert_eq(indoc! {"
4283        the quick brown foˇd over the lazy dog"});
4284        assert_eq!(cx.cx.forced_motion(), false);
4285    }
4286}