motion.rs

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