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