motion.rs

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