motion.rs

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