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