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        && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1816    {
1817        point.column += ch.len_utf8() as u32;
1818    }
1819    for _ in 0..times {
1820        let new_point = movement::find_preceding_boundary_point(
1821            &map.buffer_snapshot,
1822            point,
1823            FindRange::MultiLine,
1824            |left, right| {
1825                let left_kind = classifier.kind(left);
1826                let right_kind = classifier.kind(right);
1827                match (left_kind, right_kind) {
1828                    (CharKind::Punctuation, CharKind::Whitespace)
1829                    | (CharKind::Punctuation, CharKind::Word)
1830                    | (CharKind::Word, CharKind::Whitespace)
1831                    | (CharKind::Word, CharKind::Punctuation) => true,
1832                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1833                    _ => false,
1834                }
1835            },
1836        );
1837        if new_point == point {
1838            break;
1839        }
1840        point = new_point;
1841    }
1842    movement::saturating_left(map, point.to_display_point(map))
1843}
1844
1845fn next_subword_start(
1846    map: &DisplaySnapshot,
1847    mut point: DisplayPoint,
1848    ignore_punctuation: bool,
1849    times: usize,
1850) -> DisplayPoint {
1851    let classifier = map
1852        .buffer_snapshot
1853        .char_classifier_at(point.to_point(map))
1854        .ignore_punctuation(ignore_punctuation);
1855    for _ in 0..times {
1856        let mut crossed_newline = false;
1857        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1858            let left_kind = classifier.kind(left);
1859            let right_kind = classifier.kind(right);
1860            let at_newline = right == '\n';
1861
1862            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1863            let is_subword_start =
1864                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1865
1866            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1867                || at_newline && crossed_newline
1868                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1869
1870            crossed_newline |= at_newline;
1871            found
1872        });
1873        if point == new_point {
1874            break;
1875        }
1876        point = new_point;
1877    }
1878    point
1879}
1880
1881pub(crate) fn next_subword_end(
1882    map: &DisplaySnapshot,
1883    mut point: DisplayPoint,
1884    ignore_punctuation: bool,
1885    times: usize,
1886    allow_cross_newline: bool,
1887) -> DisplayPoint {
1888    let classifier = map
1889        .buffer_snapshot
1890        .char_classifier_at(point.to_point(map))
1891        .ignore_punctuation(ignore_punctuation);
1892    for _ in 0..times {
1893        let new_point = next_char(map, point, allow_cross_newline);
1894
1895        let mut crossed_newline = false;
1896        let mut need_backtrack = false;
1897        let new_point =
1898            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1899                let left_kind = classifier.kind(left);
1900                let right_kind = classifier.kind(right);
1901                let at_newline = right == '\n';
1902
1903                if !allow_cross_newline && at_newline {
1904                    return true;
1905                }
1906
1907                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1908                let is_subword_end =
1909                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1910
1911                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1912
1913                if found && (is_word_end || is_subword_end) {
1914                    need_backtrack = true;
1915                }
1916
1917                crossed_newline |= at_newline;
1918                found
1919            });
1920        let mut new_point = map.clip_point(new_point, Bias::Left);
1921        if need_backtrack {
1922            *new_point.column_mut() -= 1;
1923        }
1924        let new_point = map.clip_point(new_point, Bias::Left);
1925        if point == new_point {
1926            break;
1927        }
1928        point = new_point;
1929    }
1930    point
1931}
1932
1933fn previous_subword_start(
1934    map: &DisplaySnapshot,
1935    mut point: DisplayPoint,
1936    ignore_punctuation: bool,
1937    times: usize,
1938) -> DisplayPoint {
1939    let classifier = map
1940        .buffer_snapshot
1941        .char_classifier_at(point.to_point(map))
1942        .ignore_punctuation(ignore_punctuation);
1943    for _ in 0..times {
1944        let mut crossed_newline = false;
1945        // This works even though find_preceding_boundary is called for every character in the line containing
1946        // cursor because the newline is checked only once.
1947        let new_point = movement::find_preceding_boundary_display_point(
1948            map,
1949            point,
1950            FindRange::MultiLine,
1951            |left, right| {
1952                let left_kind = classifier.kind(left);
1953                let right_kind = classifier.kind(right);
1954                let at_newline = right == '\n';
1955
1956                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1957                let is_subword_start =
1958                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1959
1960                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1961                    || at_newline && crossed_newline
1962                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1963
1964                crossed_newline |= at_newline;
1965
1966                found
1967            },
1968        );
1969        if point == new_point {
1970            break;
1971        }
1972        point = new_point;
1973    }
1974    point
1975}
1976
1977fn previous_subword_end(
1978    map: &DisplaySnapshot,
1979    point: DisplayPoint,
1980    ignore_punctuation: bool,
1981    times: usize,
1982) -> DisplayPoint {
1983    let classifier = map
1984        .buffer_snapshot
1985        .char_classifier_at(point.to_point(map))
1986        .ignore_punctuation(ignore_punctuation);
1987    let mut point = point.to_point(map);
1988
1989    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1990        && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1991    {
1992        point.column += ch.len_utf8() as u32;
1993    }
1994    for _ in 0..times {
1995        let new_point = movement::find_preceding_boundary_point(
1996            &map.buffer_snapshot,
1997            point,
1998            FindRange::MultiLine,
1999            |left, right| {
2000                let left_kind = classifier.kind(left);
2001                let right_kind = classifier.kind(right);
2002
2003                let is_subword_end =
2004                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
2005
2006                if is_subword_end {
2007                    return true;
2008                }
2009
2010                match (left_kind, right_kind) {
2011                    (CharKind::Word, CharKind::Whitespace)
2012                    | (CharKind::Word, CharKind::Punctuation) => true,
2013                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2014                    _ => false,
2015                }
2016            },
2017        );
2018        if new_point == point {
2019            break;
2020        }
2021        point = new_point;
2022    }
2023    movement::saturating_left(map, point.to_display_point(map))
2024}
2025
2026pub(crate) fn first_non_whitespace(
2027    map: &DisplaySnapshot,
2028    display_lines: bool,
2029    from: DisplayPoint,
2030) -> DisplayPoint {
2031    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2032    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2033    for (ch, offset) in map.buffer_chars_at(start_offset) {
2034        if ch == '\n' {
2035            return from;
2036        }
2037
2038        start_offset = offset;
2039
2040        if classifier.kind(ch) != CharKind::Whitespace {
2041            break;
2042        }
2043    }
2044
2045    start_offset.to_display_point(map)
2046}
2047
2048pub(crate) fn last_non_whitespace(
2049    map: &DisplaySnapshot,
2050    from: DisplayPoint,
2051    count: usize,
2052) -> DisplayPoint {
2053    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2054    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2055
2056    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2057    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2058        && classifier.kind(ch) != CharKind::Whitespace
2059    {
2060        return end_of_line.to_display_point(map);
2061    }
2062
2063    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2064        if ch == '\n' {
2065            break;
2066        }
2067        end_of_line = offset;
2068        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2069            break;
2070        }
2071    }
2072
2073    end_of_line.to_display_point(map)
2074}
2075
2076pub(crate) fn start_of_line(
2077    map: &DisplaySnapshot,
2078    display_lines: bool,
2079    point: DisplayPoint,
2080) -> DisplayPoint {
2081    if display_lines {
2082        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2083    } else {
2084        map.prev_line_boundary(point.to_point(map)).1
2085    }
2086}
2087
2088pub(crate) fn middle_of_line(
2089    map: &DisplaySnapshot,
2090    display_lines: bool,
2091    point: DisplayPoint,
2092    times: Option<usize>,
2093) -> DisplayPoint {
2094    let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2095        times as f64 / 100.
2096    } else {
2097        0.5
2098    };
2099    if display_lines {
2100        map.clip_point(
2101            DisplayPoint::new(
2102                point.row(),
2103                (map.line_len(point.row()) as f64 * percent) as u32,
2104            ),
2105            Bias::Left,
2106        )
2107    } else {
2108        let mut buffer_point = point.to_point(map);
2109        buffer_point.column = (map
2110            .buffer_snapshot
2111            .line_len(MultiBufferRow(buffer_point.row)) as f64
2112            * percent) as u32;
2113
2114        map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2115    }
2116}
2117
2118pub(crate) fn end_of_line(
2119    map: &DisplaySnapshot,
2120    display_lines: bool,
2121    mut point: DisplayPoint,
2122    times: usize,
2123) -> DisplayPoint {
2124    if times > 1 {
2125        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2126    }
2127    if display_lines {
2128        map.clip_point(
2129            DisplayPoint::new(point.row(), map.line_len(point.row())),
2130            Bias::Left,
2131        )
2132    } else {
2133        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2134    }
2135}
2136
2137pub(crate) fn sentence_backwards(
2138    map: &DisplaySnapshot,
2139    point: DisplayPoint,
2140    mut times: usize,
2141) -> DisplayPoint {
2142    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2143    let mut chars = map.reverse_buffer_chars_at(start).peekable();
2144
2145    let mut was_newline = map
2146        .buffer_chars_at(start)
2147        .next()
2148        .is_some_and(|(c, _)| c == '\n');
2149
2150    while let Some((ch, offset)) = chars.next() {
2151        let start_of_next_sentence = if was_newline && ch == '\n' {
2152            Some(offset + ch.len_utf8())
2153        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2154            Some(next_non_blank(map, offset + ch.len_utf8()))
2155        } else if ch == '.' || ch == '?' || ch == '!' {
2156            start_of_next_sentence(map, offset + ch.len_utf8())
2157        } else {
2158            None
2159        };
2160
2161        if let Some(start_of_next_sentence) = start_of_next_sentence {
2162            if start_of_next_sentence < start {
2163                times = times.saturating_sub(1);
2164            }
2165            if times == 0 || offset == 0 {
2166                return map.clip_point(
2167                    start_of_next_sentence
2168                        .to_offset(&map.buffer_snapshot)
2169                        .to_display_point(map),
2170                    Bias::Left,
2171                );
2172            }
2173        }
2174        if was_newline {
2175            start = offset;
2176        }
2177        was_newline = ch == '\n';
2178    }
2179
2180    DisplayPoint::zero()
2181}
2182
2183pub(crate) fn sentence_forwards(
2184    map: &DisplaySnapshot,
2185    point: DisplayPoint,
2186    mut times: usize,
2187) -> DisplayPoint {
2188    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2189    let mut chars = map.buffer_chars_at(start).peekable();
2190
2191    let mut was_newline = map
2192        .reverse_buffer_chars_at(start)
2193        .next()
2194        .is_some_and(|(c, _)| c == '\n')
2195        && chars.peek().is_some_and(|(c, _)| *c == '\n');
2196
2197    while let Some((ch, offset)) = chars.next() {
2198        if was_newline && ch == '\n' {
2199            continue;
2200        }
2201        let start_of_next_sentence = if was_newline {
2202            Some(next_non_blank(map, offset))
2203        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2204            Some(next_non_blank(map, offset + ch.len_utf8()))
2205        } else if ch == '.' || ch == '?' || ch == '!' {
2206            start_of_next_sentence(map, offset + ch.len_utf8())
2207        } else {
2208            None
2209        };
2210
2211        if let Some(start_of_next_sentence) = start_of_next_sentence {
2212            times = times.saturating_sub(1);
2213            if times == 0 {
2214                return map.clip_point(
2215                    start_of_next_sentence
2216                        .to_offset(&map.buffer_snapshot)
2217                        .to_display_point(map),
2218                    Bias::Right,
2219                );
2220            }
2221        }
2222
2223        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2224    }
2225
2226    map.max_point()
2227}
2228
2229fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2230    for (c, o) in map.buffer_chars_at(start) {
2231        if c == '\n' || !c.is_whitespace() {
2232            return o;
2233        }
2234    }
2235
2236    map.buffer_snapshot.len()
2237}
2238
2239// given the offset after a ., !, or ? find the start of the next sentence.
2240// if this is not a sentence boundary, returns None.
2241fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2242    let chars = map.buffer_chars_at(end_of_sentence);
2243    let mut seen_space = false;
2244
2245    for (char, offset) in chars {
2246        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2247            continue;
2248        }
2249
2250        if char == '\n' && seen_space {
2251            return Some(offset);
2252        } else if char.is_whitespace() {
2253            seen_space = true;
2254        } else if seen_space {
2255            return Some(offset);
2256        } else {
2257            return None;
2258        }
2259    }
2260
2261    Some(map.buffer_snapshot.len())
2262}
2263
2264fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2265    let point = map.display_point_to_point(display_point, Bias::Left);
2266    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2267        return display_point;
2268    };
2269    let offset = excerpt.buffer().point_to_offset(
2270        excerpt
2271            .buffer()
2272            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2273    );
2274    let buffer_range = excerpt.buffer_range();
2275    if offset >= buffer_range.start && offset <= buffer_range.end {
2276        let point = map
2277            .buffer_snapshot
2278            .offset_to_point(excerpt.map_offset_from_buffer(offset));
2279        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2280    }
2281    let mut last_position = None;
2282    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2283        let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
2284            ..language::ToOffset::to_offset(&range.context.end, buffer);
2285        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2286            let text_anchor = buffer.anchor_after(offset);
2287            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2288            return anchor.to_display_point(map);
2289        } else if offset <= excerpt_range.start {
2290            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2291            return anchor.to_display_point(map);
2292        } else {
2293            last_position = Some(Anchor::in_buffer(
2294                excerpt,
2295                buffer.remote_id(),
2296                range.context.end,
2297            ));
2298        }
2299    }
2300
2301    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2302    last_point.column = point.column;
2303
2304    map.clip_point(
2305        map.point_to_display_point(
2306            map.buffer_snapshot.clip_point(point, Bias::Left),
2307            Bias::Left,
2308        ),
2309        Bias::Left,
2310    )
2311}
2312
2313fn start_of_document(
2314    map: &DisplaySnapshot,
2315    display_point: DisplayPoint,
2316    maybe_times: Option<usize>,
2317) -> DisplayPoint {
2318    if let Some(times) = maybe_times {
2319        return go_to_line(map, display_point, times);
2320    }
2321
2322    let point = map.display_point_to_point(display_point, Bias::Left);
2323    let mut first_point = Point::zero();
2324    first_point.column = point.column;
2325
2326    map.clip_point(
2327        map.point_to_display_point(
2328            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2329            Bias::Left,
2330        ),
2331        Bias::Left,
2332    )
2333}
2334
2335fn end_of_document(
2336    map: &DisplaySnapshot,
2337    display_point: DisplayPoint,
2338    maybe_times: Option<usize>,
2339) -> DisplayPoint {
2340    if let Some(times) = maybe_times {
2341        return go_to_line(map, display_point, times);
2342    };
2343    let point = map.display_point_to_point(display_point, Bias::Left);
2344    let mut last_point = map.buffer_snapshot.max_point();
2345    last_point.column = point.column;
2346
2347    map.clip_point(
2348        map.point_to_display_point(
2349            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2350            Bias::Left,
2351        ),
2352        Bias::Left,
2353    )
2354}
2355
2356fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2357    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2358    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2359
2360    if head > outer.start && head < inner.start {
2361        let mut offset = inner.end.to_offset(map, Bias::Left);
2362        for c in map.buffer_snapshot.chars_at(offset) {
2363            if c == '/' || c == '\n' || c == '>' {
2364                return Some(offset.to_display_point(map));
2365            }
2366            offset += c.len_utf8();
2367        }
2368    } else {
2369        let mut offset = outer.start.to_offset(map, Bias::Left);
2370        for c in map.buffer_snapshot.chars_at(offset) {
2371            offset += c.len_utf8();
2372            if c == '<' || c == '\n' {
2373                return Some(offset.to_display_point(map));
2374            }
2375        }
2376    }
2377
2378    None
2379}
2380
2381fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2382    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2383    let display_point = map.clip_at_line_end(display_point);
2384    let point = display_point.to_point(map);
2385    let offset = point.to_offset(&map.buffer_snapshot);
2386
2387    // Ensure the range is contained by the current line.
2388    let mut line_end = map.next_line_boundary(point).0;
2389    if line_end == point {
2390        line_end = map.max_point().to_point(map);
2391    }
2392
2393    if let Some((opening_range, closing_range)) = map
2394        .buffer_snapshot
2395        .innermost_enclosing_bracket_ranges(offset..offset, None)
2396    {
2397        if opening_range.contains(&offset) {
2398            return closing_range.start.to_display_point(map);
2399        } else if closing_range.contains(&offset) {
2400            return opening_range.start.to_display_point(map);
2401        }
2402    }
2403
2404    let line_range = map.prev_line_boundary(point).0..line_end;
2405    let visible_line_range =
2406        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2407    let ranges = map
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    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
2642/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2643pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2644    if smartcase {
2645        if target.is_uppercase() {
2646            target == other
2647        } else {
2648            target == other.to_ascii_lowercase()
2649        }
2650    } else {
2651        target == other
2652    }
2653}
2654
2655fn sneak(
2656    map: &DisplaySnapshot,
2657    from: DisplayPoint,
2658    first_target: char,
2659    second_target: char,
2660    times: usize,
2661    smartcase: bool,
2662) -> Option<DisplayPoint> {
2663    let mut to = from;
2664    let mut found = false;
2665
2666    for _ in 0..times {
2667        found = false;
2668        let new_to = find_boundary(
2669            map,
2670            movement::right(map, to),
2671            FindRange::MultiLine,
2672            |left, right| {
2673                found = is_character_match(first_target, left, smartcase)
2674                    && is_character_match(second_target, right, smartcase);
2675                found
2676            },
2677        );
2678        if to == new_to {
2679            break;
2680        }
2681        to = new_to;
2682    }
2683
2684    if found {
2685        Some(movement::left(map, to))
2686    } else {
2687        None
2688    }
2689}
2690
2691fn sneak_backward(
2692    map: &DisplaySnapshot,
2693    from: DisplayPoint,
2694    first_target: char,
2695    second_target: char,
2696    times: usize,
2697    smartcase: bool,
2698) -> Option<DisplayPoint> {
2699    let mut to = from;
2700    let mut found = false;
2701
2702    for _ in 0..times {
2703        found = false;
2704        let new_to =
2705            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2706                found = is_character_match(first_target, left, smartcase)
2707                    && is_character_match(second_target, right, smartcase);
2708                found
2709            });
2710        if to == new_to {
2711            break;
2712        }
2713        to = new_to;
2714    }
2715
2716    if found {
2717        Some(movement::left(map, to))
2718    } else {
2719        None
2720    }
2721}
2722
2723fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2724    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2725    first_non_whitespace(map, false, correct_line)
2726}
2727
2728fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2729    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2730    first_non_whitespace(map, false, correct_line)
2731}
2732
2733fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2734    let correct_line = start_of_relative_buffer_row(map, point, 0);
2735    right(map, correct_line, times.saturating_sub(1))
2736}
2737
2738pub(crate) fn next_line_end(
2739    map: &DisplaySnapshot,
2740    mut point: DisplayPoint,
2741    times: usize,
2742) -> DisplayPoint {
2743    if times > 1 {
2744        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2745    }
2746    end_of_line(map, false, point, 1)
2747}
2748
2749fn window_top(
2750    map: &DisplaySnapshot,
2751    point: DisplayPoint,
2752    text_layout_details: &TextLayoutDetails,
2753    mut times: usize,
2754) -> (DisplayPoint, SelectionGoal) {
2755    let first_visible_line = text_layout_details
2756        .scroll_anchor
2757        .anchor
2758        .to_display_point(map);
2759
2760    if first_visible_line.row() != DisplayRow(0)
2761        && text_layout_details.vertical_scroll_margin as usize > times
2762    {
2763        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2764    }
2765
2766    if let Some(visible_rows) = text_layout_details.visible_rows {
2767        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2768        let new_row = (first_visible_line.row().0 + (times as u32))
2769            .min(bottom_row)
2770            .min(map.max_point().row().0);
2771        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2772
2773        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2774        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2775    } else {
2776        let new_row =
2777            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2778        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2779
2780        let new_point = DisplayPoint::new(new_row, new_col);
2781        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2782    }
2783}
2784
2785fn window_middle(
2786    map: &DisplaySnapshot,
2787    point: DisplayPoint,
2788    text_layout_details: &TextLayoutDetails,
2789) -> (DisplayPoint, SelectionGoal) {
2790    if let Some(visible_rows) = text_layout_details.visible_rows {
2791        let first_visible_line = text_layout_details
2792            .scroll_anchor
2793            .anchor
2794            .to_display_point(map);
2795
2796        let max_visible_rows =
2797            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2798
2799        let new_row =
2800            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2801        let new_row = DisplayRow(new_row);
2802        let new_col = point.column().min(map.line_len(new_row));
2803        let new_point = DisplayPoint::new(new_row, new_col);
2804        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2805    } else {
2806        (point, SelectionGoal::None)
2807    }
2808}
2809
2810fn window_bottom(
2811    map: &DisplaySnapshot,
2812    point: DisplayPoint,
2813    text_layout_details: &TextLayoutDetails,
2814    mut times: usize,
2815) -> (DisplayPoint, SelectionGoal) {
2816    if let Some(visible_rows) = text_layout_details.visible_rows {
2817        let first_visible_line = text_layout_details
2818            .scroll_anchor
2819            .anchor
2820            .to_display_point(map);
2821        let bottom_row = first_visible_line.row().0
2822            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2823        if bottom_row < map.max_point().row().0
2824            && text_layout_details.vertical_scroll_margin as usize > times
2825        {
2826            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2827        }
2828        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2829        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2830        {
2831            first_visible_line.row()
2832        } else {
2833            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2834        };
2835        let new_col = point.column().min(map.line_len(new_row));
2836        let new_point = DisplayPoint::new(new_row, new_col);
2837        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2838    } else {
2839        (point, SelectionGoal::None)
2840    }
2841}
2842
2843fn method_motion(
2844    map: &DisplaySnapshot,
2845    mut display_point: DisplayPoint,
2846    times: usize,
2847    direction: Direction,
2848    is_start: bool,
2849) -> DisplayPoint {
2850    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2851        return display_point;
2852    };
2853
2854    for _ in 0..times {
2855        let point = map.display_point_to_point(display_point, Bias::Left);
2856        let offset = point.to_offset(&map.buffer_snapshot);
2857        let range = if direction == Direction::Prev {
2858            0..offset
2859        } else {
2860            offset..buffer.len()
2861        };
2862
2863        let possibilities = buffer
2864            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2865            .filter_map(|(range, object)| {
2866                if !matches!(object, language::TextObject::AroundFunction) {
2867                    return None;
2868                }
2869
2870                let relevant = if is_start { range.start } else { range.end };
2871                if direction == Direction::Prev && relevant < offset {
2872                    Some(relevant)
2873                } else if direction == Direction::Next && relevant > offset + 1 {
2874                    Some(relevant)
2875                } else {
2876                    None
2877                }
2878            });
2879
2880        let dest = if direction == Direction::Prev {
2881            possibilities.max().unwrap_or(offset)
2882        } else {
2883            possibilities.min().unwrap_or(offset)
2884        };
2885        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2886        if new_point == display_point {
2887            break;
2888        }
2889        display_point = new_point;
2890    }
2891    display_point
2892}
2893
2894fn comment_motion(
2895    map: &DisplaySnapshot,
2896    mut display_point: DisplayPoint,
2897    times: usize,
2898    direction: Direction,
2899) -> DisplayPoint {
2900    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2901        return display_point;
2902    };
2903
2904    for _ in 0..times {
2905        let point = map.display_point_to_point(display_point, Bias::Left);
2906        let offset = point.to_offset(&map.buffer_snapshot);
2907        let range = if direction == Direction::Prev {
2908            0..offset
2909        } else {
2910            offset..buffer.len()
2911        };
2912
2913        let possibilities = buffer
2914            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2915            .filter_map(|(range, object)| {
2916                if !matches!(object, language::TextObject::AroundComment) {
2917                    return None;
2918                }
2919
2920                let relevant = if direction == Direction::Prev {
2921                    range.start
2922                } else {
2923                    range.end
2924                };
2925                if direction == Direction::Prev && relevant < offset {
2926                    Some(relevant)
2927                } else if direction == Direction::Next && relevant > offset + 1 {
2928                    Some(relevant)
2929                } else {
2930                    None
2931                }
2932            });
2933
2934        let dest = if direction == Direction::Prev {
2935            possibilities.max().unwrap_or(offset)
2936        } else {
2937            possibilities.min().unwrap_or(offset)
2938        };
2939        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2940        if new_point == display_point {
2941            break;
2942        }
2943        display_point = new_point;
2944    }
2945
2946    display_point
2947}
2948
2949fn section_motion(
2950    map: &DisplaySnapshot,
2951    mut display_point: DisplayPoint,
2952    times: usize,
2953    direction: Direction,
2954    is_start: bool,
2955) -> DisplayPoint {
2956    if map.buffer_snapshot.as_singleton().is_some() {
2957        for _ in 0..times {
2958            let offset = map
2959                .display_point_to_point(display_point, Bias::Left)
2960                .to_offset(&map.buffer_snapshot);
2961            let range = if direction == Direction::Prev {
2962                0..offset
2963            } else {
2964                offset..map.buffer_snapshot.len()
2965            };
2966
2967            // we set a max start depth here because we want a section to only be "top level"
2968            // similar to vim's default of '{' in the first column.
2969            // (and without it, ]] at the start of editor.rs is -very- slow)
2970            let mut possibilities = map
2971                .buffer_snapshot
2972                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2973                .filter(|(_, object)| {
2974                    matches!(
2975                        object,
2976                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2977                    )
2978                })
2979                .collect::<Vec<_>>();
2980            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2981            let mut prev_end = None;
2982            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2983                if t == language::TextObject::AroundFunction
2984                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2985                {
2986                    return None;
2987                }
2988                prev_end = Some(range.end);
2989
2990                let relevant = if is_start { range.start } else { range.end };
2991                if direction == Direction::Prev && relevant < offset {
2992                    Some(relevant)
2993                } else if direction == Direction::Next && relevant > offset + 1 {
2994                    Some(relevant)
2995                } else {
2996                    None
2997                }
2998            });
2999
3000            let offset = if direction == Direction::Prev {
3001                possibilities.max().unwrap_or(0)
3002            } else {
3003                possibilities.min().unwrap_or(map.buffer_snapshot.len())
3004            };
3005
3006            let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3007            if new_point == display_point {
3008                break;
3009            }
3010            display_point = new_point;
3011        }
3012        return display_point;
3013    };
3014
3015    for _ in 0..times {
3016        let next_point = if is_start {
3017            movement::start_of_excerpt(map, display_point, direction)
3018        } else {
3019            movement::end_of_excerpt(map, display_point, direction)
3020        };
3021        if next_point == display_point {
3022            break;
3023        }
3024        display_point = next_point;
3025    }
3026
3027    display_point
3028}
3029
3030fn matches_indent_type(
3031    target_indent: &text::LineIndent,
3032    current_indent: &text::LineIndent,
3033    indent_type: IndentType,
3034) -> bool {
3035    match indent_type {
3036        IndentType::Lesser => {
3037            target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3038        }
3039        IndentType::Greater => {
3040            target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3041        }
3042        IndentType::Same => {
3043            target_indent.spaces == current_indent.spaces
3044                && target_indent.tabs == current_indent.tabs
3045        }
3046    }
3047}
3048
3049fn indent_motion(
3050    map: &DisplaySnapshot,
3051    mut display_point: DisplayPoint,
3052    times: usize,
3053    direction: Direction,
3054    indent_type: IndentType,
3055) -> DisplayPoint {
3056    let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3057    let current_row = MultiBufferRow(buffer_point.row);
3058    let current_indent = map.line_indent_for_buffer_row(current_row);
3059    if current_indent.is_line_empty() {
3060        return display_point;
3061    }
3062    let max_row = map.max_point().to_point(map).row;
3063
3064    for _ in 0..times {
3065        let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3066
3067        let target_row = match direction {
3068            Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3069                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3070                !indent.is_line_empty()
3071                    && matches_indent_type(&indent, &current_indent, indent_type)
3072            }),
3073            Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3074                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3075                !indent.is_line_empty()
3076                    && matches_indent_type(&indent, &current_indent, indent_type)
3077            }),
3078        }
3079        .unwrap_or(current_buffer_row);
3080
3081        let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3082        let new_point = first_non_whitespace(map, false, new_point);
3083        if new_point == display_point {
3084            break;
3085        }
3086        display_point = new_point;
3087    }
3088    display_point
3089}
3090
3091#[cfg(test)]
3092mod test {
3093
3094    use crate::{
3095        state::Mode,
3096        test::{NeovimBackedTestContext, VimTestContext},
3097    };
3098    use editor::display_map::Inlay;
3099    use indoc::indoc;
3100    use language::Point;
3101    use multi_buffer::MultiBufferRow;
3102
3103    #[gpui::test]
3104    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3105        let mut cx = NeovimBackedTestContext::new(cx).await;
3106
3107        let initial_state = indoc! {r"ˇabc
3108            def
3109
3110            paragraph
3111            the second
3112
3113
3114
3115            third and
3116            final"};
3117
3118        // goes down once
3119        cx.set_shared_state(initial_state).await;
3120        cx.simulate_shared_keystrokes("}").await;
3121        cx.shared_state().await.assert_eq(indoc! {r"abc
3122            def
3123            ˇ
3124            paragraph
3125            the second
3126
3127
3128
3129            third and
3130            final"});
3131
3132        // goes up once
3133        cx.simulate_shared_keystrokes("{").await;
3134        cx.shared_state().await.assert_eq(initial_state);
3135
3136        // goes down twice
3137        cx.simulate_shared_keystrokes("2 }").await;
3138        cx.shared_state().await.assert_eq(indoc! {r"abc
3139            def
3140
3141            paragraph
3142            the second
3143            ˇ
3144
3145
3146            third and
3147            final"});
3148
3149        // goes down over multiple blanks
3150        cx.simulate_shared_keystrokes("}").await;
3151        cx.shared_state().await.assert_eq(indoc! {r"abc
3152                def
3153
3154                paragraph
3155                the second
3156
3157
3158
3159                third and
3160                finaˇl"});
3161
3162        // goes up twice
3163        cx.simulate_shared_keystrokes("2 {").await;
3164        cx.shared_state().await.assert_eq(indoc! {r"abc
3165                def
3166                ˇ
3167                paragraph
3168                the second
3169
3170
3171
3172                third and
3173                final"});
3174    }
3175
3176    #[gpui::test]
3177    async fn test_matching(cx: &mut gpui::TestAppContext) {
3178        let mut cx = NeovimBackedTestContext::new(cx).await;
3179
3180        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3181                do(something(with<Types>.and_arrays[0, 2]))
3182            }"})
3183            .await;
3184        cx.simulate_shared_keystrokes("%").await;
3185        cx.shared_state()
3186            .await
3187            .assert_eq(indoc! {r"func (a stringˇ) {
3188                do(something(with<Types>.and_arrays[0, 2]))
3189            }"});
3190
3191        // test it works on the last character of the line
3192        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3193            do(something(with<Types>.and_arrays[0, 2]))
3194            }"})
3195            .await;
3196        cx.simulate_shared_keystrokes("%").await;
3197        cx.shared_state()
3198            .await
3199            .assert_eq(indoc! {r"func (a string) {
3200            do(something(with<Types>.and_arrays[0, 2]))
3201            ˇ}"});
3202
3203        // test it works on immediate nesting
3204        cx.set_shared_state("ˇ{()}").await;
3205        cx.simulate_shared_keystrokes("%").await;
3206        cx.shared_state().await.assert_eq("{()ˇ}");
3207        cx.simulate_shared_keystrokes("%").await;
3208        cx.shared_state().await.assert_eq("ˇ{()}");
3209
3210        // test it works on immediate nesting inside braces
3211        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3212        cx.simulate_shared_keystrokes("%").await;
3213        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3214
3215        // test it jumps to the next paren on a line
3216        cx.set_shared_state("func ˇboop() {\n}").await;
3217        cx.simulate_shared_keystrokes("%").await;
3218        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3219    }
3220
3221    #[gpui::test]
3222    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3223        let mut cx = NeovimBackedTestContext::new(cx).await;
3224
3225        // test it works with curly braces
3226        cx.set_shared_state(indoc! {r"func (a string) {
3227                do(something(with<Types>.anˇd_arrays[0, 2]))
3228            }"})
3229            .await;
3230        cx.simulate_shared_keystrokes("] }").await;
3231        cx.shared_state()
3232            .await
3233            .assert_eq(indoc! {r"func (a string) {
3234                do(something(with<Types>.and_arrays[0, 2]))
3235            ˇ}"});
3236
3237        // test it works with brackets
3238        cx.set_shared_state(indoc! {r"func (a string) {
3239                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3240            }"})
3241            .await;
3242        cx.simulate_shared_keystrokes("] )").await;
3243        cx.shared_state()
3244            .await
3245            .assert_eq(indoc! {r"func (a string) {
3246                do(something(with<Types>.and_arrays[0, 2])ˇ)
3247            }"});
3248
3249        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3250            .await;
3251        cx.simulate_shared_keystrokes("] )").await;
3252        cx.shared_state()
3253            .await
3254            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3255
3256        // test it works on immediate nesting
3257        cx.set_shared_state("{ˇ {}{}}").await;
3258        cx.simulate_shared_keystrokes("] }").await;
3259        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3260        cx.set_shared_state("(ˇ ()())").await;
3261        cx.simulate_shared_keystrokes("] )").await;
3262        cx.shared_state().await.assert_eq("( ()()ˇ)");
3263
3264        // test it works on immediate nesting inside braces
3265        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3266        cx.simulate_shared_keystrokes("] }").await;
3267        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3268        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3269        cx.simulate_shared_keystrokes("] )").await;
3270        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3271    }
3272
3273    #[gpui::test]
3274    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3275        let mut cx = NeovimBackedTestContext::new(cx).await;
3276
3277        // test it works with curly braces
3278        cx.set_shared_state(indoc! {r"func (a string) {
3279                do(something(with<Types>.anˇd_arrays[0, 2]))
3280            }"})
3281            .await;
3282        cx.simulate_shared_keystrokes("[ {").await;
3283        cx.shared_state()
3284            .await
3285            .assert_eq(indoc! {r"func (a string) ˇ{
3286                do(something(with<Types>.and_arrays[0, 2]))
3287            }"});
3288
3289        // test it works with brackets
3290        cx.set_shared_state(indoc! {r"func (a string) {
3291                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3292            }"})
3293            .await;
3294        cx.simulate_shared_keystrokes("[ (").await;
3295        cx.shared_state()
3296            .await
3297            .assert_eq(indoc! {r"func (a string) {
3298                doˇ(something(with<Types>.and_arrays[0, 2]))
3299            }"});
3300
3301        // test it works on immediate nesting
3302        cx.set_shared_state("{{}{} ˇ }").await;
3303        cx.simulate_shared_keystrokes("[ {").await;
3304        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3305        cx.set_shared_state("(()() ˇ )").await;
3306        cx.simulate_shared_keystrokes("[ (").await;
3307        cx.shared_state().await.assert_eq("ˇ(()()  )");
3308
3309        // test it works on immediate nesting inside braces
3310        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3311        cx.simulate_shared_keystrokes("[ {").await;
3312        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3313        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3314        cx.simulate_shared_keystrokes("[ (").await;
3315        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3316    }
3317
3318    #[gpui::test]
3319    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3320        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3321
3322        cx.neovim.exec("set filetype=html").await;
3323
3324        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3325        cx.simulate_shared_keystrokes("%").await;
3326        cx.shared_state()
3327            .await
3328            .assert_eq(indoc! {r"<body><ˇ/body>"});
3329        cx.simulate_shared_keystrokes("%").await;
3330
3331        // test jumping backwards
3332        cx.shared_state()
3333            .await
3334            .assert_eq(indoc! {r"<ˇbody></body>"});
3335
3336        // test self-closing tags
3337        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3338        cx.simulate_shared_keystrokes("%").await;
3339        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3340
3341        // test tag with attributes
3342        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3343            </div>
3344            "})
3345            .await;
3346        cx.simulate_shared_keystrokes("%").await;
3347        cx.shared_state()
3348            .await
3349            .assert_eq(indoc! {r"<div class='test' id='main'>
3350            <ˇ/div>
3351            "});
3352
3353        // test multi-line self-closing tag
3354        cx.set_shared_state(indoc! {r#"<a>
3355            <br
3356                test = "test"
3357            /ˇ>
3358        </a>"#})
3359            .await;
3360        cx.simulate_shared_keystrokes("%").await;
3361        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3362            ˇ<br
3363                test = "test"
3364            />
3365        </a>"#});
3366    }
3367
3368    #[gpui::test]
3369    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3370        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3371
3372        // test brackets within tags
3373        cx.set_shared_state(indoc! {r"function f() {
3374            return (
3375                <div rules={ˇ[{ a: 1 }]}>
3376                    <h1>test</h1>
3377                </div>
3378            );
3379        }"})
3380            .await;
3381        cx.simulate_shared_keystrokes("%").await;
3382        cx.shared_state().await.assert_eq(indoc! {r"function f() {
3383            return (
3384                <div rules={[{ a: 1 }ˇ]}>
3385                    <h1>test</h1>
3386                </div>
3387            );
3388        }"});
3389    }
3390
3391    #[gpui::test]
3392    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3393        let mut cx = NeovimBackedTestContext::new(cx).await;
3394
3395        // f and F
3396        cx.set_shared_state("ˇone two three four").await;
3397        cx.simulate_shared_keystrokes("f o").await;
3398        cx.shared_state().await.assert_eq("one twˇo three four");
3399        cx.simulate_shared_keystrokes(",").await;
3400        cx.shared_state().await.assert_eq("ˇone two three four");
3401        cx.simulate_shared_keystrokes("2 ;").await;
3402        cx.shared_state().await.assert_eq("one two three fˇour");
3403        cx.simulate_shared_keystrokes("shift-f e").await;
3404        cx.shared_state().await.assert_eq("one two threˇe four");
3405        cx.simulate_shared_keystrokes("2 ;").await;
3406        cx.shared_state().await.assert_eq("onˇe two three four");
3407        cx.simulate_shared_keystrokes(",").await;
3408        cx.shared_state().await.assert_eq("one two thrˇee four");
3409
3410        // t and T
3411        cx.set_shared_state("ˇone two three four").await;
3412        cx.simulate_shared_keystrokes("t o").await;
3413        cx.shared_state().await.assert_eq("one tˇwo three four");
3414        cx.simulate_shared_keystrokes(",").await;
3415        cx.shared_state().await.assert_eq("oˇne two three four");
3416        cx.simulate_shared_keystrokes("2 ;").await;
3417        cx.shared_state().await.assert_eq("one two three ˇfour");
3418        cx.simulate_shared_keystrokes("shift-t e").await;
3419        cx.shared_state().await.assert_eq("one two threeˇ four");
3420        cx.simulate_shared_keystrokes("3 ;").await;
3421        cx.shared_state().await.assert_eq("oneˇ two three four");
3422        cx.simulate_shared_keystrokes(",").await;
3423        cx.shared_state().await.assert_eq("one two thˇree four");
3424    }
3425
3426    #[gpui::test]
3427    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3428        let mut cx = NeovimBackedTestContext::new(cx).await;
3429        let initial_state = indoc! {r"something(ˇfoo)"};
3430        cx.set_shared_state(initial_state).await;
3431        cx.simulate_shared_keystrokes("}").await;
3432        cx.shared_state().await.assert_eq("something(fooˇ)");
3433    }
3434
3435    #[gpui::test]
3436    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3437        let mut cx = NeovimBackedTestContext::new(cx).await;
3438        cx.set_shared_state("ˇone\n  two\nthree").await;
3439        cx.simulate_shared_keystrokes("enter").await;
3440        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3441    }
3442
3443    #[gpui::test]
3444    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3445        let mut cx = NeovimBackedTestContext::new(cx).await;
3446        cx.set_shared_state("ˇ one\n two \nthree").await;
3447        cx.simulate_shared_keystrokes("g _").await;
3448        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3449
3450        cx.set_shared_state("ˇ one \n two \nthree").await;
3451        cx.simulate_shared_keystrokes("g _").await;
3452        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3453        cx.simulate_shared_keystrokes("2 g _").await;
3454        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3455    }
3456
3457    #[gpui::test]
3458    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3459        let mut cx = NeovimBackedTestContext::new(cx).await;
3460        let initial_state = indoc! {r"abc
3461          def
3462          paragraph
3463          the second
3464          third ˇand
3465          final"};
3466
3467        cx.set_shared_state(initial_state).await;
3468        cx.simulate_shared_keystrokes("shift-h").await;
3469        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3470          def
3471          paragraph
3472          the second
3473          third and
3474          final"});
3475
3476        // clip point
3477        cx.set_shared_state(indoc! {r"
3478          1 2 3
3479          4 5 6
3480          7 8 ˇ9
3481          "})
3482            .await;
3483        cx.simulate_shared_keystrokes("shift-h").await;
3484        cx.shared_state().await.assert_eq(indoc! {"
3485          1 2 ˇ3
3486          4 5 6
3487          7 8 9
3488          "});
3489
3490        cx.set_shared_state(indoc! {r"
3491          1 2 3
3492          4 5 6
3493          ˇ7 8 9
3494          "})
3495            .await;
3496        cx.simulate_shared_keystrokes("shift-h").await;
3497        cx.shared_state().await.assert_eq(indoc! {"
3498          ˇ1 2 3
3499          4 5 6
3500          7 8 9
3501          "});
3502
3503        cx.set_shared_state(indoc! {r"
3504          1 2 3
3505          4 5 ˇ6
3506          7 8 9"})
3507            .await;
3508        cx.simulate_shared_keystrokes("9 shift-h").await;
3509        cx.shared_state().await.assert_eq(indoc! {"
3510          1 2 3
3511          4 5 6
3512          7 8 ˇ9"});
3513    }
3514
3515    #[gpui::test]
3516    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3517        let mut cx = NeovimBackedTestContext::new(cx).await;
3518        let initial_state = indoc! {r"abˇc
3519          def
3520          paragraph
3521          the second
3522          third and
3523          final"};
3524
3525        cx.set_shared_state(initial_state).await;
3526        cx.simulate_shared_keystrokes("shift-m").await;
3527        cx.shared_state().await.assert_eq(indoc! {r"abc
3528          def
3529          paˇragraph
3530          the second
3531          third and
3532          final"});
3533
3534        cx.set_shared_state(indoc! {r"
3535          1 2 3
3536          4 5 6
3537          7 8 ˇ9
3538          "})
3539            .await;
3540        cx.simulate_shared_keystrokes("shift-m").await;
3541        cx.shared_state().await.assert_eq(indoc! {"
3542          1 2 3
3543          4 5 ˇ6
3544          7 8 9
3545          "});
3546        cx.set_shared_state(indoc! {r"
3547          1 2 3
3548          4 5 6
3549          ˇ7 8 9
3550          "})
3551            .await;
3552        cx.simulate_shared_keystrokes("shift-m").await;
3553        cx.shared_state().await.assert_eq(indoc! {"
3554          1 2 3
3555          ˇ4 5 6
3556          7 8 9
3557          "});
3558        cx.set_shared_state(indoc! {r"
3559          ˇ1 2 3
3560          4 5 6
3561          7 8 9
3562          "})
3563            .await;
3564        cx.simulate_shared_keystrokes("shift-m").await;
3565        cx.shared_state().await.assert_eq(indoc! {"
3566          1 2 3
3567          ˇ4 5 6
3568          7 8 9
3569          "});
3570        cx.set_shared_state(indoc! {r"
3571          1 2 3
3572          ˇ4 5 6
3573          7 8 9
3574          "})
3575            .await;
3576        cx.simulate_shared_keystrokes("shift-m").await;
3577        cx.shared_state().await.assert_eq(indoc! {"
3578          1 2 3
3579          ˇ4 5 6
3580          7 8 9
3581          "});
3582        cx.set_shared_state(indoc! {r"
3583          1 2 3
3584          4 5 ˇ6
3585          7 8 9
3586          "})
3587            .await;
3588        cx.simulate_shared_keystrokes("shift-m").await;
3589        cx.shared_state().await.assert_eq(indoc! {"
3590          1 2 3
3591          4 5 ˇ6
3592          7 8 9
3593          "});
3594    }
3595
3596    #[gpui::test]
3597    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3598        let mut cx = NeovimBackedTestContext::new(cx).await;
3599        let initial_state = indoc! {r"abc
3600          deˇf
3601          paragraph
3602          the second
3603          third and
3604          final"};
3605
3606        cx.set_shared_state(initial_state).await;
3607        cx.simulate_shared_keystrokes("shift-l").await;
3608        cx.shared_state().await.assert_eq(indoc! {r"abc
3609          def
3610          paragraph
3611          the second
3612          third and
3613          fiˇnal"});
3614
3615        cx.set_shared_state(indoc! {r"
3616          1 2 3
3617          4 5 ˇ6
3618          7 8 9
3619          "})
3620            .await;
3621        cx.simulate_shared_keystrokes("shift-l").await;
3622        cx.shared_state().await.assert_eq(indoc! {"
3623          1 2 3
3624          4 5 6
3625          7 8 9
3626          ˇ"});
3627
3628        cx.set_shared_state(indoc! {r"
3629          1 2 3
3630          ˇ4 5 6
3631          7 8 9
3632          "})
3633            .await;
3634        cx.simulate_shared_keystrokes("shift-l").await;
3635        cx.shared_state().await.assert_eq(indoc! {"
3636          1 2 3
3637          4 5 6
3638          7 8 9
3639          ˇ"});
3640
3641        cx.set_shared_state(indoc! {r"
3642          1 2 ˇ3
3643          4 5 6
3644          7 8 9
3645          "})
3646            .await;
3647        cx.simulate_shared_keystrokes("shift-l").await;
3648        cx.shared_state().await.assert_eq(indoc! {"
3649          1 2 3
3650          4 5 6
3651          7 8 9
3652          ˇ"});
3653
3654        cx.set_shared_state(indoc! {r"
3655          ˇ1 2 3
3656          4 5 6
3657          7 8 9
3658          "})
3659            .await;
3660        cx.simulate_shared_keystrokes("shift-l").await;
3661        cx.shared_state().await.assert_eq(indoc! {"
3662          1 2 3
3663          4 5 6
3664          7 8 9
3665          ˇ"});
3666
3667        cx.set_shared_state(indoc! {r"
3668          1 2 3
3669          4 5 ˇ6
3670          7 8 9
3671          "})
3672            .await;
3673        cx.simulate_shared_keystrokes("9 shift-l").await;
3674        cx.shared_state().await.assert_eq(indoc! {"
3675          1 2 ˇ3
3676          4 5 6
3677          7 8 9
3678          "});
3679    }
3680
3681    #[gpui::test]
3682    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3683        let mut cx = NeovimBackedTestContext::new(cx).await;
3684        cx.set_shared_state(indoc! {r"
3685        456 5ˇ67 678
3686        "})
3687            .await;
3688        cx.simulate_shared_keystrokes("g e").await;
3689        cx.shared_state().await.assert_eq(indoc! {"
3690        45ˇ6 567 678
3691        "});
3692
3693        // Test times
3694        cx.set_shared_state(indoc! {r"
3695        123 234 345
3696        456 5ˇ67 678
3697        "})
3698            .await;
3699        cx.simulate_shared_keystrokes("4 g e").await;
3700        cx.shared_state().await.assert_eq(indoc! {"
3701        12ˇ3 234 345
3702        456 567 678
3703        "});
3704
3705        // With punctuation
3706        cx.set_shared_state(indoc! {r"
3707        123 234 345
3708        4;5.6 5ˇ67 678
3709        789 890 901
3710        "})
3711            .await;
3712        cx.simulate_shared_keystrokes("g e").await;
3713        cx.shared_state().await.assert_eq(indoc! {"
3714          123 234 345
3715          4;5.ˇ6 567 678
3716          789 890 901
3717        "});
3718
3719        // With punctuation and count
3720        cx.set_shared_state(indoc! {r"
3721        123 234 345
3722        4;5.6 5ˇ67 678
3723        789 890 901
3724        "})
3725            .await;
3726        cx.simulate_shared_keystrokes("5 g e").await;
3727        cx.shared_state().await.assert_eq(indoc! {"
3728          123 234 345
3729          ˇ4;5.6 567 678
3730          789 890 901
3731        "});
3732
3733        // newlines
3734        cx.set_shared_state(indoc! {r"
3735        123 234 345
3736
3737        78ˇ9 890 901
3738        "})
3739            .await;
3740        cx.simulate_shared_keystrokes("g e").await;
3741        cx.shared_state().await.assert_eq(indoc! {"
3742          123 234 345
3743          ˇ
3744          789 890 901
3745        "});
3746        cx.simulate_shared_keystrokes("g e").await;
3747        cx.shared_state().await.assert_eq(indoc! {"
3748          123 234 34ˇ5
3749
3750          789 890 901
3751        "});
3752
3753        // With punctuation
3754        cx.set_shared_state(indoc! {r"
3755        123 234 345
3756        4;5.ˇ6 567 678
3757        789 890 901
3758        "})
3759            .await;
3760        cx.simulate_shared_keystrokes("g shift-e").await;
3761        cx.shared_state().await.assert_eq(indoc! {"
3762          123 234 34ˇ5
3763          4;5.6 567 678
3764          789 890 901
3765        "});
3766
3767        // With multi byte char
3768        cx.set_shared_state(indoc! {r"
3769        bar ˇó
3770        "})
3771            .await;
3772        cx.simulate_shared_keystrokes("g e").await;
3773        cx.shared_state().await.assert_eq(indoc! {"
3774        baˇr ó
3775        "});
3776    }
3777
3778    #[gpui::test]
3779    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3780        let mut cx = NeovimBackedTestContext::new(cx).await;
3781
3782        cx.set_shared_state(indoc! {"
3783            fn aˇ() {
3784              return
3785            }
3786        "})
3787            .await;
3788        cx.simulate_shared_keystrokes("v $ %").await;
3789        cx.shared_state().await.assert_eq(indoc! {"
3790            fn a«() {
3791              return
3792            }ˇ»
3793        "});
3794    }
3795
3796    #[gpui::test]
3797    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3798        let mut cx = VimTestContext::new(cx, true).await;
3799
3800        cx.set_state(
3801            indoc! {"
3802                struct Foo {
3803                ˇ
3804                }
3805            "},
3806            Mode::Normal,
3807        );
3808
3809        cx.update_editor(|editor, _window, cx| {
3810            let range = editor.selections.newest_anchor().range();
3811            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3812            let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
3813            editor.splice_inlays(&[], vec![inlay], cx);
3814        });
3815
3816        cx.simulate_keystrokes("j");
3817        cx.assert_state(
3818            indoc! {"
3819                struct Foo {
3820
3821                ˇ}
3822            "},
3823            Mode::Normal,
3824        );
3825    }
3826
3827    #[gpui::test]
3828    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3829        let mut cx = VimTestContext::new(cx, true).await;
3830
3831        cx.set_state(
3832            indoc! {"
3833            ˇstruct Foo {
3834
3835            }
3836        "},
3837            Mode::Normal,
3838        );
3839        cx.update_editor(|editor, _window, cx| {
3840            let snapshot = editor.buffer().read(cx).snapshot(cx);
3841            let end_of_line =
3842                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3843            let inlay_text = " hint";
3844            let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
3845            editor.splice_inlays(&[], vec![inlay], cx);
3846        });
3847        cx.simulate_keystrokes("$");
3848        cx.assert_state(
3849            indoc! {"
3850            struct Foo ˇ{
3851
3852            }
3853        "},
3854            Mode::Normal,
3855        );
3856    }
3857
3858    #[gpui::test]
3859    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3860        let mut cx = NeovimBackedTestContext::new(cx).await;
3861        // Normal mode
3862        cx.set_shared_state(indoc! {"
3863            The ˇquick brown
3864            fox jumps over
3865            the lazy dog
3866            The quick brown
3867            fox jumps over
3868            the lazy dog
3869            The quick brown
3870            fox jumps over
3871            the lazy dog"})
3872            .await;
3873        cx.simulate_shared_keystrokes("2 0 %").await;
3874        cx.shared_state().await.assert_eq(indoc! {"
3875            The quick brown
3876            fox ˇjumps over
3877            the lazy dog
3878            The quick brown
3879            fox jumps over
3880            the lazy dog
3881            The quick brown
3882            fox jumps over
3883            the lazy dog"});
3884
3885        cx.simulate_shared_keystrokes("2 5 %").await;
3886        cx.shared_state().await.assert_eq(indoc! {"
3887            The quick brown
3888            fox jumps over
3889            the ˇlazy dog
3890            The quick brown
3891            fox jumps over
3892            the lazy dog
3893            The quick brown
3894            fox jumps over
3895            the lazy dog"});
3896
3897        cx.simulate_shared_keystrokes("7 5 %").await;
3898        cx.shared_state().await.assert_eq(indoc! {"
3899            The quick brown
3900            fox jumps over
3901            the lazy dog
3902            The quick brown
3903            fox jumps over
3904            the lazy dog
3905            The ˇquick brown
3906            fox jumps over
3907            the lazy dog"});
3908
3909        // Visual mode
3910        cx.set_shared_state(indoc! {"
3911            The ˇquick brown
3912            fox jumps over
3913            the lazy dog
3914            The quick brown
3915            fox jumps over
3916            the lazy dog
3917            The quick brown
3918            fox jumps over
3919            the lazy dog"})
3920            .await;
3921        cx.simulate_shared_keystrokes("v 5 0 %").await;
3922        cx.shared_state().await.assert_eq(indoc! {"
3923            The «quick brown
3924            fox jumps over
3925            the lazy dog
3926            The quick brown
3927            fox jˇ»umps over
3928            the lazy dog
3929            The quick brown
3930            fox jumps over
3931            the lazy dog"});
3932
3933        cx.set_shared_state(indoc! {"
3934            The ˇquick brown
3935            fox jumps over
3936            the lazy dog
3937            The quick brown
3938            fox jumps over
3939            the lazy dog
3940            The quick brown
3941            fox jumps over
3942            the lazy dog"})
3943            .await;
3944        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3945        cx.shared_state().await.assert_eq(indoc! {"
3946            The «quick brown
3947            fox jumps over
3948            the lazy dog
3949            The quick brown
3950            fox jumps over
3951            the lazy dog
3952            The quick brown
3953            fox jumps over
3954            the lˇ»azy dog"});
3955    }
3956
3957    #[gpui::test]
3958    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3959        let mut cx = NeovimBackedTestContext::new(cx).await;
3960
3961        cx.set_shared_state("ˇπππππ").await;
3962        cx.simulate_shared_keystrokes("3 space").await;
3963        cx.shared_state().await.assert_eq("πππˇππ");
3964    }
3965
3966    #[gpui::test]
3967    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3968        let mut cx = NeovimBackedTestContext::new(cx).await;
3969
3970        cx.set_shared_state(indoc! {"
3971            ππππˇπ
3972            πanotherline"})
3973            .await;
3974        cx.simulate_shared_keystrokes("4 space").await;
3975        cx.shared_state().await.assert_eq(indoc! {"
3976            πππππ
3977            πanˇotherline"});
3978    }
3979
3980    #[gpui::test]
3981    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3982        let mut cx = NeovimBackedTestContext::new(cx).await;
3983
3984        cx.set_shared_state(indoc! {"
3985                        ππππ
3986                        πanˇotherline"})
3987            .await;
3988        cx.simulate_shared_keystrokes("4 backspace").await;
3989        cx.shared_state().await.assert_eq(indoc! {"
3990                        πππˇπ
3991                        πanotherline"});
3992    }
3993
3994    #[gpui::test]
3995    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3996        let mut cx = VimTestContext::new(cx, true).await;
3997        cx.set_state(
3998            indoc! {
3999                "func empty(a string) bool {
4000                     ˇif a == \"\" {
4001                         return true
4002                     }
4003                     return false
4004                }"
4005            },
4006            Mode::Normal,
4007        );
4008        cx.simulate_keystrokes("[ -");
4009        cx.assert_state(
4010            indoc! {
4011                "ˇfunc empty(a string) bool {
4012                     if a == \"\" {
4013                         return true
4014                     }
4015                     return false
4016                }"
4017            },
4018            Mode::Normal,
4019        );
4020        cx.simulate_keystrokes("] =");
4021        cx.assert_state(
4022            indoc! {
4023                "func empty(a string) bool {
4024                     if a == \"\" {
4025                         return true
4026                     }
4027                     return false
4028                ˇ}"
4029            },
4030            Mode::Normal,
4031        );
4032        cx.simulate_keystrokes("[ +");
4033        cx.assert_state(
4034            indoc! {
4035                "func empty(a string) bool {
4036                     if a == \"\" {
4037                         return true
4038                     }
4039                     ˇreturn false
4040                }"
4041            },
4042            Mode::Normal,
4043        );
4044        cx.simulate_keystrokes("2 [ =");
4045        cx.assert_state(
4046            indoc! {
4047                "func empty(a string) bool {
4048                     ˇif a == \"\" {
4049                         return true
4050                     }
4051                     return false
4052                }"
4053            },
4054            Mode::Normal,
4055        );
4056        cx.simulate_keystrokes("] +");
4057        cx.assert_state(
4058            indoc! {
4059                "func empty(a string) bool {
4060                     if a == \"\" {
4061                         ˇreturn true
4062                     }
4063                     return false
4064                }"
4065            },
4066            Mode::Normal,
4067        );
4068        cx.simulate_keystrokes("] -");
4069        cx.assert_state(
4070            indoc! {
4071                "func empty(a string) bool {
4072                     if a == \"\" {
4073                         return true
4074                     ˇ}
4075                     return false
4076                }"
4077            },
4078            Mode::Normal,
4079        );
4080    }
4081
4082    #[gpui::test]
4083    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4084        let mut cx = NeovimBackedTestContext::new(cx).await;
4085        cx.set_shared_state("abˇc").await;
4086        cx.simulate_shared_keystrokes("delete").await;
4087        cx.shared_state().await.assert_eq("aˇb");
4088    }
4089
4090    #[gpui::test]
4091    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4092        let mut cx = NeovimBackedTestContext::new(cx).await;
4093
4094        cx.set_shared_state(indoc! {"
4095             ˇthe quick brown fox
4096             jumped over the lazy dog"})
4097            .await;
4098        cx.simulate_shared_keystrokes("d v 0").await;
4099        cx.shared_state().await.assert_eq(indoc! {"
4100             ˇhe quick brown fox
4101             jumped over the lazy dog"});
4102        assert_eq!(cx.cx.forced_motion(), false);
4103
4104        cx.set_shared_state(indoc! {"
4105            the quick bˇrown fox
4106            jumped over the lazy dog"})
4107            .await;
4108        cx.simulate_shared_keystrokes("d v 0").await;
4109        cx.shared_state().await.assert_eq(indoc! {"
4110            ˇown fox
4111            jumped over the lazy dog"});
4112        assert_eq!(cx.cx.forced_motion(), false);
4113
4114        cx.set_shared_state(indoc! {"
4115            the quick brown foˇx
4116            jumped over the lazy dog"})
4117            .await;
4118        cx.simulate_shared_keystrokes("d v 0").await;
4119        cx.shared_state().await.assert_eq(indoc! {"
4120            ˇ
4121            jumped over the lazy dog"});
4122        assert_eq!(cx.cx.forced_motion(), false);
4123    }
4124
4125    #[gpui::test]
4126    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4127        let mut cx = NeovimBackedTestContext::new(cx).await;
4128
4129        cx.set_shared_state(indoc! {"
4130             ˇthe quick brown fox
4131             jumped over the lazy dog"})
4132            .await;
4133        cx.simulate_shared_keystrokes("d v g shift-m").await;
4134        cx.shared_state().await.assert_eq(indoc! {"
4135             ˇbrown fox
4136             jumped over the lazy dog"});
4137        assert_eq!(cx.cx.forced_motion(), false);
4138
4139        cx.set_shared_state(indoc! {"
4140            the quick bˇrown fox
4141            jumped over the lazy dog"})
4142            .await;
4143        cx.simulate_shared_keystrokes("d v g shift-m").await;
4144        cx.shared_state().await.assert_eq(indoc! {"
4145            the quickˇown fox
4146            jumped over the lazy dog"});
4147        assert_eq!(cx.cx.forced_motion(), false);
4148
4149        cx.set_shared_state(indoc! {"
4150            the quick brown foˇx
4151            jumped over the lazy dog"})
4152            .await;
4153        cx.simulate_shared_keystrokes("d v g shift-m").await;
4154        cx.shared_state().await.assert_eq(indoc! {"
4155            the quicˇk
4156            jumped over the lazy dog"});
4157        assert_eq!(cx.cx.forced_motion(), false);
4158
4159        cx.set_shared_state(indoc! {"
4160            ˇthe quick brown fox
4161            jumped over the lazy dog"})
4162            .await;
4163        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4164        cx.shared_state().await.assert_eq(indoc! {"
4165            ˇ fox
4166            jumped over the lazy dog"});
4167        assert_eq!(cx.cx.forced_motion(), false);
4168
4169        cx.set_shared_state(indoc! {"
4170            ˇthe quick brown fox
4171            jumped over the lazy dog"})
4172            .await;
4173        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4174        cx.shared_state().await.assert_eq(indoc! {"
4175            ˇuick brown fox
4176            jumped over the lazy dog"});
4177        assert_eq!(cx.cx.forced_motion(), false);
4178    }
4179
4180    #[gpui::test]
4181    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4182        let mut cx = NeovimBackedTestContext::new(cx).await;
4183
4184        cx.set_shared_state(indoc! {"
4185             the quick brown foˇx
4186             jumped over the lazy dog"})
4187            .await;
4188        cx.simulate_shared_keystrokes("d v $").await;
4189        cx.shared_state().await.assert_eq(indoc! {"
4190             the quick brown foˇx
4191             jumped over the lazy dog"});
4192        assert_eq!(cx.cx.forced_motion(), false);
4193
4194        cx.set_shared_state(indoc! {"
4195             ˇthe quick brown fox
4196             jumped over the lazy dog"})
4197            .await;
4198        cx.simulate_shared_keystrokes("d v $").await;
4199        cx.shared_state().await.assert_eq(indoc! {"
4200             ˇx
4201             jumped over the lazy dog"});
4202        assert_eq!(cx.cx.forced_motion(), false);
4203    }
4204
4205    #[gpui::test]
4206    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4207        let mut cx = NeovimBackedTestContext::new(cx).await;
4208
4209        cx.set_shared_state(indoc! {"
4210               ˇthe quick brown fox
4211               jumped over the lazy dog"})
4212            .await;
4213        cx.simulate_shared_keystrokes("y v j p").await;
4214        cx.shared_state().await.assert_eq(indoc! {"
4215               the quick brown fox
4216               ˇthe quick brown fox
4217               jumped over the lazy dog"});
4218        assert_eq!(cx.cx.forced_motion(), false);
4219
4220        cx.set_shared_state(indoc! {"
4221              the quick bˇrown fox
4222              jumped over the lazy dog"})
4223            .await;
4224        cx.simulate_shared_keystrokes("y v j p").await;
4225        cx.shared_state().await.assert_eq(indoc! {"
4226              the quick brˇrown fox
4227              jumped overown fox
4228              jumped over the lazy dog"});
4229        assert_eq!(cx.cx.forced_motion(), false);
4230
4231        cx.set_shared_state(indoc! {"
4232             the quick brown foˇx
4233             jumped over the lazy dog"})
4234            .await;
4235        cx.simulate_shared_keystrokes("y v j p").await;
4236        cx.shared_state().await.assert_eq(indoc! {"
4237             the quick brown foxˇx
4238             jumped over the la
4239             jumped over the lazy dog"});
4240        assert_eq!(cx.cx.forced_motion(), false);
4241
4242        cx.set_shared_state(indoc! {"
4243             the quick brown fox
4244             jˇumped over the lazy dog"})
4245            .await;
4246        cx.simulate_shared_keystrokes("y v k p").await;
4247        cx.shared_state().await.assert_eq(indoc! {"
4248            thˇhe quick brown fox
4249            je quick brown fox
4250            jumped over the lazy dog"});
4251        assert_eq!(cx.cx.forced_motion(), false);
4252    }
4253
4254    #[gpui::test]
4255    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4256        let mut cx = NeovimBackedTestContext::new(cx).await;
4257
4258        cx.set_shared_state(indoc! {"
4259              ˇthe quick brown fox
4260              jumped over the lazy dog"})
4261            .await;
4262        cx.simulate_shared_keystrokes("d v e").await;
4263        cx.shared_state().await.assert_eq(indoc! {"
4264              ˇe quick brown fox
4265              jumped over the lazy dog"});
4266        assert_eq!(cx.cx.forced_motion(), false);
4267
4268        cx.set_shared_state(indoc! {"
4269              the quick bˇrown fox
4270              jumped over the lazy dog"})
4271            .await;
4272        cx.simulate_shared_keystrokes("d v e").await;
4273        cx.shared_state().await.assert_eq(indoc! {"
4274              the quick bˇn fox
4275              jumped over the lazy dog"});
4276        assert_eq!(cx.cx.forced_motion(), false);
4277
4278        cx.set_shared_state(indoc! {"
4279             the quick brown foˇx
4280             jumped over the lazy dog"})
4281            .await;
4282        cx.simulate_shared_keystrokes("d v e").await;
4283        cx.shared_state().await.assert_eq(indoc! {"
4284        the quick brown foˇd over the lazy dog"});
4285        assert_eq!(cx.cx.forced_motion(), false);
4286    }
4287}