motion.rs

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