motion.rs

   1use editor::{
   2    display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
   3    movement::{
   4        self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
   5    },
   6    scroll::Autoscroll,
   7    Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset,
   8};
   9use gpui::{actions, impl_actions, px, ViewContext};
  10use language::{CharKind, Point, Selection, SelectionGoal};
  11use multi_buffer::MultiBufferRow;
  12use serde::Deserialize;
  13use std::ops::Range;
  14use workspace::searchable::Direction;
  15
  16use crate::{
  17    normal::mark,
  18    state::{Mode, Operator},
  19    surrounds::SurroundsType,
  20    Vim,
  21};
  22
  23#[derive(Clone, Debug, PartialEq, Eq)]
  24pub enum Motion {
  25    Left,
  26    Backspace,
  27    Down {
  28        display_lines: bool,
  29    },
  30    Up {
  31        display_lines: bool,
  32    },
  33    Right,
  34    Space,
  35    NextWordStart {
  36        ignore_punctuation: bool,
  37    },
  38    NextWordEnd {
  39        ignore_punctuation: bool,
  40    },
  41    PreviousWordStart {
  42        ignore_punctuation: bool,
  43    },
  44    PreviousWordEnd {
  45        ignore_punctuation: bool,
  46    },
  47    NextSubwordStart {
  48        ignore_punctuation: bool,
  49    },
  50    NextSubwordEnd {
  51        ignore_punctuation: bool,
  52    },
  53    PreviousSubwordStart {
  54        ignore_punctuation: bool,
  55    },
  56    PreviousSubwordEnd {
  57        ignore_punctuation: bool,
  58    },
  59    FirstNonWhitespace {
  60        display_lines: bool,
  61    },
  62    CurrentLine,
  63    StartOfLine {
  64        display_lines: bool,
  65    },
  66    EndOfLine {
  67        display_lines: bool,
  68    },
  69    SentenceBackward,
  70    SentenceForward,
  71    StartOfParagraph,
  72    EndOfParagraph,
  73    StartOfDocument,
  74    EndOfDocument,
  75    Matching,
  76    UnmatchedForward {
  77        char: char,
  78    },
  79    UnmatchedBackward {
  80        char: char,
  81    },
  82    FindForward {
  83        before: bool,
  84        char: char,
  85        mode: FindRange,
  86        smartcase: bool,
  87    },
  88    FindBackward {
  89        after: bool,
  90        char: char,
  91        mode: FindRange,
  92        smartcase: bool,
  93    },
  94    RepeatFind {
  95        last_find: Box<Motion>,
  96    },
  97    RepeatFindReversed {
  98        last_find: Box<Motion>,
  99    },
 100    NextLineStart,
 101    PreviousLineStart,
 102    StartOfLineDownward,
 103    EndOfLineDownward,
 104    GoToColumn,
 105    WindowTop,
 106    WindowMiddle,
 107    WindowBottom,
 108    NextSectionStart,
 109    NextSectionEnd,
 110    PreviousSectionStart,
 111    PreviousSectionEnd,
 112    NextMethodStart,
 113    NextMethodEnd,
 114    PreviousMethodStart,
 115    PreviousMethodEnd,
 116    NextComment,
 117    PreviousComment,
 118
 119    // we don't have a good way to run a search synchronously, so
 120    // we handle search motions by running the search async and then
 121    // calling back into motion with this
 122    ZedSearchResult {
 123        prior_selections: Vec<Range<Anchor>>,
 124        new_selections: Vec<Range<Anchor>>,
 125    },
 126    Jump {
 127        anchor: Anchor,
 128        line: bool,
 129    },
 130}
 131
 132#[derive(Clone, Deserialize, PartialEq)]
 133#[serde(rename_all = "camelCase")]
 134struct NextWordStart {
 135    #[serde(default)]
 136    ignore_punctuation: bool,
 137}
 138
 139#[derive(Clone, Deserialize, PartialEq)]
 140#[serde(rename_all = "camelCase")]
 141struct NextWordEnd {
 142    #[serde(default)]
 143    ignore_punctuation: bool,
 144}
 145
 146#[derive(Clone, Deserialize, PartialEq)]
 147#[serde(rename_all = "camelCase")]
 148struct PreviousWordStart {
 149    #[serde(default)]
 150    ignore_punctuation: bool,
 151}
 152
 153#[derive(Clone, Deserialize, PartialEq)]
 154#[serde(rename_all = "camelCase")]
 155struct PreviousWordEnd {
 156    #[serde(default)]
 157    ignore_punctuation: bool,
 158}
 159
 160#[derive(Clone, Deserialize, PartialEq)]
 161#[serde(rename_all = "camelCase")]
 162pub(crate) struct NextSubwordStart {
 163    #[serde(default)]
 164    pub(crate) ignore_punctuation: bool,
 165}
 166
 167#[derive(Clone, Deserialize, PartialEq)]
 168#[serde(rename_all = "camelCase")]
 169pub(crate) struct NextSubwordEnd {
 170    #[serde(default)]
 171    pub(crate) ignore_punctuation: bool,
 172}
 173
 174#[derive(Clone, Deserialize, PartialEq)]
 175#[serde(rename_all = "camelCase")]
 176pub(crate) struct PreviousSubwordStart {
 177    #[serde(default)]
 178    pub(crate) ignore_punctuation: bool,
 179}
 180
 181#[derive(Clone, Deserialize, PartialEq)]
 182#[serde(rename_all = "camelCase")]
 183pub(crate) struct PreviousSubwordEnd {
 184    #[serde(default)]
 185    pub(crate) ignore_punctuation: bool,
 186}
 187
 188#[derive(Clone, Deserialize, PartialEq)]
 189#[serde(rename_all = "camelCase")]
 190pub(crate) struct Up {
 191    #[serde(default)]
 192    pub(crate) display_lines: bool,
 193}
 194
 195#[derive(Clone, Deserialize, PartialEq)]
 196#[serde(rename_all = "camelCase")]
 197pub(crate) struct Down {
 198    #[serde(default)]
 199    pub(crate) display_lines: bool,
 200}
 201
 202#[derive(Clone, Deserialize, PartialEq)]
 203#[serde(rename_all = "camelCase")]
 204struct FirstNonWhitespace {
 205    #[serde(default)]
 206    display_lines: bool,
 207}
 208
 209#[derive(Clone, Deserialize, PartialEq)]
 210#[serde(rename_all = "camelCase")]
 211struct EndOfLine {
 212    #[serde(default)]
 213    display_lines: bool,
 214}
 215
 216#[derive(Clone, Deserialize, PartialEq)]
 217#[serde(rename_all = "camelCase")]
 218pub struct StartOfLine {
 219    #[serde(default)]
 220    pub(crate) display_lines: bool,
 221}
 222
 223#[derive(Clone, Deserialize, PartialEq)]
 224#[serde(rename_all = "camelCase")]
 225struct UnmatchedForward {
 226    #[serde(default)]
 227    char: char,
 228}
 229
 230#[derive(Clone, Deserialize, PartialEq)]
 231#[serde(rename_all = "camelCase")]
 232struct UnmatchedBackward {
 233    #[serde(default)]
 234    char: char,
 235}
 236
 237impl_actions!(
 238    vim,
 239    [
 240        StartOfLine,
 241        EndOfLine,
 242        FirstNonWhitespace,
 243        Down,
 244        Up,
 245        NextWordStart,
 246        NextWordEnd,
 247        PreviousWordStart,
 248        PreviousWordEnd,
 249        NextSubwordStart,
 250        NextSubwordEnd,
 251        PreviousSubwordStart,
 252        PreviousSubwordEnd,
 253        UnmatchedForward,
 254        UnmatchedBackward
 255    ]
 256);
 257
 258actions!(
 259    vim,
 260    [
 261        Left,
 262        Backspace,
 263        Right,
 264        Space,
 265        CurrentLine,
 266        SentenceForward,
 267        SentenceBackward,
 268        StartOfParagraph,
 269        EndOfParagraph,
 270        StartOfDocument,
 271        EndOfDocument,
 272        Matching,
 273        NextLineStart,
 274        PreviousLineStart,
 275        StartOfLineDownward,
 276        EndOfLineDownward,
 277        GoToColumn,
 278        RepeatFind,
 279        RepeatFindReversed,
 280        WindowTop,
 281        WindowMiddle,
 282        WindowBottom,
 283        NextSectionStart,
 284        NextSectionEnd,
 285        PreviousSectionStart,
 286        PreviousSectionEnd,
 287        NextMethodStart,
 288        NextMethodEnd,
 289        PreviousMethodStart,
 290        PreviousMethodEnd,
 291        NextComment,
 292        PreviousComment,
 293    ]
 294);
 295
 296pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 297    Vim::action(editor, cx, |vim, _: &Left, cx| vim.motion(Motion::Left, cx));
 298    Vim::action(editor, cx, |vim, _: &Backspace, cx| {
 299        vim.motion(Motion::Backspace, cx)
 300    });
 301    Vim::action(editor, cx, |vim, action: &Down, cx| {
 302        vim.motion(
 303            Motion::Down {
 304                display_lines: action.display_lines,
 305            },
 306            cx,
 307        )
 308    });
 309    Vim::action(editor, cx, |vim, action: &Up, cx| {
 310        vim.motion(
 311            Motion::Up {
 312                display_lines: action.display_lines,
 313            },
 314            cx,
 315        )
 316    });
 317    Vim::action(editor, cx, |vim, _: &Right, cx| {
 318        vim.motion(Motion::Right, cx)
 319    });
 320    Vim::action(editor, cx, |vim, _: &Space, cx| {
 321        vim.motion(Motion::Space, cx)
 322    });
 323    Vim::action(editor, cx, |vim, action: &FirstNonWhitespace, cx| {
 324        vim.motion(
 325            Motion::FirstNonWhitespace {
 326                display_lines: action.display_lines,
 327            },
 328            cx,
 329        )
 330    });
 331    Vim::action(editor, cx, |vim, action: &StartOfLine, cx| {
 332        vim.motion(
 333            Motion::StartOfLine {
 334                display_lines: action.display_lines,
 335            },
 336            cx,
 337        )
 338    });
 339    Vim::action(editor, cx, |vim, action: &EndOfLine, cx| {
 340        vim.motion(
 341            Motion::EndOfLine {
 342                display_lines: action.display_lines,
 343            },
 344            cx,
 345        )
 346    });
 347    Vim::action(editor, cx, |vim, _: &CurrentLine, cx| {
 348        vim.motion(Motion::CurrentLine, cx)
 349    });
 350    Vim::action(editor, cx, |vim, _: &StartOfParagraph, cx| {
 351        vim.motion(Motion::StartOfParagraph, cx)
 352    });
 353    Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| {
 354        vim.motion(Motion::EndOfParagraph, cx)
 355    });
 356
 357    Vim::action(editor, cx, |vim, _: &SentenceForward, cx| {
 358        vim.motion(Motion::SentenceForward, cx)
 359    });
 360    Vim::action(editor, cx, |vim, _: &SentenceBackward, cx| {
 361        vim.motion(Motion::SentenceBackward, cx)
 362    });
 363    Vim::action(editor, cx, |vim, _: &StartOfDocument, cx| {
 364        vim.motion(Motion::StartOfDocument, cx)
 365    });
 366    Vim::action(editor, cx, |vim, _: &EndOfDocument, cx| {
 367        vim.motion(Motion::EndOfDocument, cx)
 368    });
 369    Vim::action(editor, cx, |vim, _: &Matching, cx| {
 370        vim.motion(Motion::Matching, cx)
 371    });
 372    Vim::action(
 373        editor,
 374        cx,
 375        |vim, &UnmatchedForward { char }: &UnmatchedForward, cx| {
 376            vim.motion(Motion::UnmatchedForward { char }, cx)
 377        },
 378    );
 379    Vim::action(
 380        editor,
 381        cx,
 382        |vim, &UnmatchedBackward { char }: &UnmatchedBackward, cx| {
 383            vim.motion(Motion::UnmatchedBackward { char }, cx)
 384        },
 385    );
 386    Vim::action(
 387        editor,
 388        cx,
 389        |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, cx| {
 390            vim.motion(Motion::NextWordStart { ignore_punctuation }, cx)
 391        },
 392    );
 393    Vim::action(
 394        editor,
 395        cx,
 396        |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx| {
 397            vim.motion(Motion::NextWordEnd { ignore_punctuation }, cx)
 398        },
 399    );
 400    Vim::action(
 401        editor,
 402        cx,
 403        |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, cx| {
 404            vim.motion(Motion::PreviousWordStart { ignore_punctuation }, cx)
 405        },
 406    );
 407    Vim::action(
 408        editor,
 409        cx,
 410        |vim, &PreviousWordEnd { ignore_punctuation }, cx| {
 411            vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
 412        },
 413    );
 414    Vim::action(
 415        editor,
 416        cx,
 417        |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx| {
 418            vim.motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
 419        },
 420    );
 421    Vim::action(
 422        editor,
 423        cx,
 424        |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx| {
 425            vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
 426        },
 427    );
 428    Vim::action(
 429        editor,
 430        cx,
 431        |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, cx| {
 432            vim.motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx)
 433        },
 434    );
 435    Vim::action(
 436        editor,
 437        cx,
 438        |vim, &PreviousSubwordEnd { ignore_punctuation }, cx| {
 439            vim.motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
 440        },
 441    );
 442    Vim::action(editor, cx, |vim, &NextLineStart, cx| {
 443        vim.motion(Motion::NextLineStart, cx)
 444    });
 445    Vim::action(editor, cx, |vim, &PreviousLineStart, cx| {
 446        vim.motion(Motion::PreviousLineStart, cx)
 447    });
 448    Vim::action(editor, cx, |vim, &StartOfLineDownward, cx| {
 449        vim.motion(Motion::StartOfLineDownward, cx)
 450    });
 451    Vim::action(editor, cx, |vim, &EndOfLineDownward, cx| {
 452        vim.motion(Motion::EndOfLineDownward, cx)
 453    });
 454    Vim::action(editor, cx, |vim, &GoToColumn, cx| {
 455        vim.motion(Motion::GoToColumn, cx)
 456    });
 457
 458    Vim::action(editor, cx, |vim, _: &RepeatFind, cx| {
 459        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 460            vim.motion(Motion::RepeatFind { last_find }, cx);
 461        }
 462    });
 463
 464    Vim::action(editor, cx, |vim, _: &RepeatFindReversed, cx| {
 465        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 466            vim.motion(Motion::RepeatFindReversed { last_find }, cx);
 467        }
 468    });
 469    Vim::action(editor, cx, |vim, &WindowTop, cx| {
 470        vim.motion(Motion::WindowTop, cx)
 471    });
 472    Vim::action(editor, cx, |vim, &WindowMiddle, cx| {
 473        vim.motion(Motion::WindowMiddle, cx)
 474    });
 475    Vim::action(editor, cx, |vim, &WindowBottom, cx| {
 476        vim.motion(Motion::WindowBottom, cx)
 477    });
 478
 479    Vim::action(editor, cx, |vim, &PreviousSectionStart, cx| {
 480        vim.motion(Motion::PreviousSectionStart, cx)
 481    });
 482    Vim::action(editor, cx, |vim, &NextSectionStart, cx| {
 483        vim.motion(Motion::NextSectionStart, cx)
 484    });
 485    Vim::action(editor, cx, |vim, &PreviousSectionEnd, cx| {
 486        vim.motion(Motion::PreviousSectionEnd, cx)
 487    });
 488    Vim::action(editor, cx, |vim, &NextSectionEnd, cx| {
 489        vim.motion(Motion::NextSectionEnd, cx)
 490    });
 491    Vim::action(editor, cx, |vim, &PreviousMethodStart, cx| {
 492        vim.motion(Motion::PreviousMethodStart, cx)
 493    });
 494    Vim::action(editor, cx, |vim, &NextMethodStart, cx| {
 495        vim.motion(Motion::NextMethodStart, cx)
 496    });
 497    Vim::action(editor, cx, |vim, &PreviousMethodEnd, cx| {
 498        vim.motion(Motion::PreviousMethodEnd, cx)
 499    });
 500    Vim::action(editor, cx, |vim, &NextMethodEnd, cx| {
 501        vim.motion(Motion::NextMethodEnd, cx)
 502    });
 503    Vim::action(editor, cx, |vim, &NextComment, cx| {
 504        vim.motion(Motion::NextComment, cx)
 505    });
 506    Vim::action(editor, cx, |vim, &PreviousComment, cx| {
 507        vim.motion(Motion::PreviousComment, cx)
 508    });
 509}
 510
 511impl Vim {
 512    pub(crate) fn search_motion(&mut self, m: Motion, cx: &mut ViewContext<Self>) {
 513        if let Motion::ZedSearchResult {
 514            prior_selections, ..
 515        } = &m
 516        {
 517            match self.mode {
 518                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 519                    if !prior_selections.is_empty() {
 520                        self.update_editor(cx, |_, editor, cx| {
 521                            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 522                                s.select_ranges(prior_selections.iter().cloned())
 523                            })
 524                        });
 525                    }
 526                }
 527                Mode::Normal | Mode::Replace | Mode::Insert => {
 528                    if self.active_operator().is_none() {
 529                        return;
 530                    }
 531                }
 532            }
 533        }
 534
 535        self.motion(m, cx)
 536    }
 537
 538    pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext<Self>) {
 539        if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
 540            self.active_operator()
 541        {
 542            self.pop_operator(cx);
 543        }
 544
 545        let count = Vim::take_count(cx);
 546        let active_operator = self.active_operator();
 547        let mut waiting_operator: Option<Operator> = None;
 548        match self.mode {
 549            Mode::Normal | Mode::Replace | Mode::Insert => {
 550                if active_operator == Some(Operator::AddSurrounds { target: None }) {
 551                    waiting_operator = Some(Operator::AddSurrounds {
 552                        target: Some(SurroundsType::Motion(motion)),
 553                    });
 554                } else {
 555                    self.normal_motion(motion.clone(), active_operator.clone(), count, cx)
 556                }
 557            }
 558            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 559                self.visual_motion(motion.clone(), count, cx)
 560            }
 561        }
 562        self.clear_operator(cx);
 563        if let Some(operator) = waiting_operator {
 564            self.push_operator(operator, cx);
 565            Vim::globals(cx).pre_count = count
 566        }
 567    }
 568}
 569
 570// Motion handling is specified here:
 571// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 572impl Motion {
 573    pub fn linewise(&self) -> bool {
 574        use Motion::*;
 575        match self {
 576            Down { .. }
 577            | Up { .. }
 578            | StartOfDocument
 579            | EndOfDocument
 580            | CurrentLine
 581            | NextLineStart
 582            | PreviousLineStart
 583            | StartOfLineDownward
 584            | SentenceBackward
 585            | SentenceForward
 586            | StartOfParagraph
 587            | EndOfParagraph
 588            | WindowTop
 589            | WindowMiddle
 590            | WindowBottom
 591            | NextSectionStart
 592            | NextSectionEnd
 593            | PreviousSectionStart
 594            | PreviousSectionEnd
 595            | NextMethodStart
 596            | NextMethodEnd
 597            | PreviousMethodStart
 598            | PreviousMethodEnd
 599            | NextComment
 600            | PreviousComment
 601            | Jump { line: true, .. } => true,
 602            EndOfLine { .. }
 603            | Matching
 604            | UnmatchedForward { .. }
 605            | UnmatchedBackward { .. }
 606            | FindForward { .. }
 607            | Left
 608            | Backspace
 609            | Right
 610            | Space
 611            | StartOfLine { .. }
 612            | EndOfLineDownward
 613            | GoToColumn
 614            | NextWordStart { .. }
 615            | NextWordEnd { .. }
 616            | PreviousWordStart { .. }
 617            | PreviousWordEnd { .. }
 618            | NextSubwordStart { .. }
 619            | NextSubwordEnd { .. }
 620            | PreviousSubwordStart { .. }
 621            | PreviousSubwordEnd { .. }
 622            | FirstNonWhitespace { .. }
 623            | FindBackward { .. }
 624            | RepeatFind { .. }
 625            | RepeatFindReversed { .. }
 626            | Jump { line: false, .. }
 627            | ZedSearchResult { .. } => false,
 628        }
 629    }
 630
 631    pub fn infallible(&self) -> bool {
 632        use Motion::*;
 633        match self {
 634            StartOfDocument | EndOfDocument | CurrentLine => true,
 635            Down { .. }
 636            | Up { .. }
 637            | EndOfLine { .. }
 638            | Matching
 639            | UnmatchedForward { .. }
 640            | UnmatchedBackward { .. }
 641            | FindForward { .. }
 642            | RepeatFind { .. }
 643            | Left
 644            | Backspace
 645            | Right
 646            | Space
 647            | StartOfLine { .. }
 648            | StartOfParagraph
 649            | EndOfParagraph
 650            | SentenceBackward
 651            | SentenceForward
 652            | StartOfLineDownward
 653            | EndOfLineDownward
 654            | GoToColumn
 655            | NextWordStart { .. }
 656            | NextWordEnd { .. }
 657            | PreviousWordStart { .. }
 658            | PreviousWordEnd { .. }
 659            | NextSubwordStart { .. }
 660            | NextSubwordEnd { .. }
 661            | PreviousSubwordStart { .. }
 662            | PreviousSubwordEnd { .. }
 663            | FirstNonWhitespace { .. }
 664            | FindBackward { .. }
 665            | RepeatFindReversed { .. }
 666            | WindowTop
 667            | WindowMiddle
 668            | WindowBottom
 669            | NextLineStart
 670            | PreviousLineStart
 671            | ZedSearchResult { .. }
 672            | NextSectionStart
 673            | NextSectionEnd
 674            | PreviousSectionStart
 675            | PreviousSectionEnd
 676            | NextMethodStart
 677            | NextMethodEnd
 678            | PreviousMethodStart
 679            | PreviousMethodEnd
 680            | NextComment
 681            | PreviousComment
 682            | Jump { .. } => false,
 683        }
 684    }
 685
 686    pub fn inclusive(&self) -> bool {
 687        use Motion::*;
 688        match self {
 689            Down { .. }
 690            | Up { .. }
 691            | StartOfDocument
 692            | EndOfDocument
 693            | CurrentLine
 694            | EndOfLine { .. }
 695            | EndOfLineDownward
 696            | Matching
 697            | UnmatchedForward { .. }
 698            | UnmatchedBackward { .. }
 699            | FindForward { .. }
 700            | WindowTop
 701            | WindowMiddle
 702            | WindowBottom
 703            | NextWordEnd { .. }
 704            | PreviousWordEnd { .. }
 705            | NextSubwordEnd { .. }
 706            | PreviousSubwordEnd { .. }
 707            | NextLineStart
 708            | PreviousLineStart => true,
 709            Left
 710            | Backspace
 711            | Right
 712            | Space
 713            | StartOfLine { .. }
 714            | StartOfLineDownward
 715            | StartOfParagraph
 716            | EndOfParagraph
 717            | SentenceBackward
 718            | SentenceForward
 719            | GoToColumn
 720            | NextWordStart { .. }
 721            | PreviousWordStart { .. }
 722            | NextSubwordStart { .. }
 723            | PreviousSubwordStart { .. }
 724            | FirstNonWhitespace { .. }
 725            | FindBackward { .. }
 726            | Jump { .. }
 727            | NextSectionStart
 728            | NextSectionEnd
 729            | PreviousSectionStart
 730            | PreviousSectionEnd
 731            | NextMethodStart
 732            | NextMethodEnd
 733            | PreviousMethodStart
 734            | PreviousMethodEnd
 735            | NextComment
 736            | PreviousComment
 737            | ZedSearchResult { .. } => false,
 738            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 739                motion.inclusive()
 740            }
 741        }
 742    }
 743
 744    pub fn move_point(
 745        &self,
 746        map: &DisplaySnapshot,
 747        point: DisplayPoint,
 748        goal: SelectionGoal,
 749        maybe_times: Option<usize>,
 750        text_layout_details: &TextLayoutDetails,
 751    ) -> Option<(DisplayPoint, SelectionGoal)> {
 752        let times = maybe_times.unwrap_or(1);
 753        use Motion::*;
 754        let infallible = self.infallible();
 755        let (new_point, goal) = match self {
 756            Left => (left(map, point, times), SelectionGoal::None),
 757            Backspace => (backspace(map, point, times), SelectionGoal::None),
 758            Down {
 759                display_lines: false,
 760            } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
 761            Down {
 762                display_lines: true,
 763            } => down_display(map, point, goal, times, text_layout_details),
 764            Up {
 765                display_lines: false,
 766            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
 767            Up {
 768                display_lines: true,
 769            } => up_display(map, point, goal, times, text_layout_details),
 770            Right => (right(map, point, times), SelectionGoal::None),
 771            Space => (space(map, point, times), SelectionGoal::None),
 772            NextWordStart { ignore_punctuation } => (
 773                next_word_start(map, point, *ignore_punctuation, times),
 774                SelectionGoal::None,
 775            ),
 776            NextWordEnd { ignore_punctuation } => (
 777                next_word_end(map, point, *ignore_punctuation, times, true),
 778                SelectionGoal::None,
 779            ),
 780            PreviousWordStart { ignore_punctuation } => (
 781                previous_word_start(map, point, *ignore_punctuation, times),
 782                SelectionGoal::None,
 783            ),
 784            PreviousWordEnd { ignore_punctuation } => (
 785                previous_word_end(map, point, *ignore_punctuation, times),
 786                SelectionGoal::None,
 787            ),
 788            NextSubwordStart { ignore_punctuation } => (
 789                next_subword_start(map, point, *ignore_punctuation, times),
 790                SelectionGoal::None,
 791            ),
 792            NextSubwordEnd { ignore_punctuation } => (
 793                next_subword_end(map, point, *ignore_punctuation, times, true),
 794                SelectionGoal::None,
 795            ),
 796            PreviousSubwordStart { ignore_punctuation } => (
 797                previous_subword_start(map, point, *ignore_punctuation, times),
 798                SelectionGoal::None,
 799            ),
 800            PreviousSubwordEnd { ignore_punctuation } => (
 801                previous_subword_end(map, point, *ignore_punctuation, times),
 802                SelectionGoal::None,
 803            ),
 804            FirstNonWhitespace { display_lines } => (
 805                first_non_whitespace(map, *display_lines, point),
 806                SelectionGoal::None,
 807            ),
 808            StartOfLine { display_lines } => (
 809                start_of_line(map, *display_lines, point),
 810                SelectionGoal::None,
 811            ),
 812            EndOfLine { display_lines } => (
 813                end_of_line(map, *display_lines, point, times),
 814                SelectionGoal::None,
 815            ),
 816            SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
 817            SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
 818            StartOfParagraph => (
 819                movement::start_of_paragraph(map, point, times),
 820                SelectionGoal::None,
 821            ),
 822            EndOfParagraph => (
 823                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 824                SelectionGoal::None,
 825            ),
 826            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 827            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 828            EndOfDocument => (
 829                end_of_document(map, point, maybe_times),
 830                SelectionGoal::None,
 831            ),
 832            Matching => (matching(map, point), SelectionGoal::None),
 833            UnmatchedForward { char } => (
 834                unmatched_forward(map, point, *char, times),
 835                SelectionGoal::None,
 836            ),
 837            UnmatchedBackward { char } => (
 838                unmatched_backward(map, point, *char, times),
 839                SelectionGoal::None,
 840            ),
 841            // t f
 842            FindForward {
 843                before,
 844                char,
 845                mode,
 846                smartcase,
 847            } => {
 848                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
 849                    .map(|new_point| (new_point, SelectionGoal::None))
 850            }
 851            // T F
 852            FindBackward {
 853                after,
 854                char,
 855                mode,
 856                smartcase,
 857            } => (
 858                find_backward(map, point, *after, *char, times, *mode, *smartcase),
 859                SelectionGoal::None,
 860            ),
 861            // ; -- repeat the last find done with t, f, T, F
 862            RepeatFind { last_find } => match **last_find {
 863                Motion::FindForward {
 864                    before,
 865                    char,
 866                    mode,
 867                    smartcase,
 868                } => {
 869                    let mut new_point =
 870                        find_forward(map, point, before, char, times, mode, smartcase);
 871                    if new_point == Some(point) {
 872                        new_point =
 873                            find_forward(map, point, before, char, times + 1, mode, smartcase);
 874                    }
 875
 876                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 877                }
 878
 879                Motion::FindBackward {
 880                    after,
 881                    char,
 882                    mode,
 883                    smartcase,
 884                } => {
 885                    let mut new_point =
 886                        find_backward(map, point, after, char, times, mode, smartcase);
 887                    if new_point == point {
 888                        new_point =
 889                            find_backward(map, point, after, char, times + 1, mode, smartcase);
 890                    }
 891
 892                    (new_point, SelectionGoal::None)
 893                }
 894                _ => return None,
 895            },
 896            // , -- repeat the last find done with t, f, T, F, in opposite direction
 897            RepeatFindReversed { last_find } => match **last_find {
 898                Motion::FindForward {
 899                    before,
 900                    char,
 901                    mode,
 902                    smartcase,
 903                } => {
 904                    let mut new_point =
 905                        find_backward(map, point, before, char, times, mode, smartcase);
 906                    if new_point == point {
 907                        new_point =
 908                            find_backward(map, point, before, char, times + 1, mode, smartcase);
 909                    }
 910
 911                    (new_point, SelectionGoal::None)
 912                }
 913
 914                Motion::FindBackward {
 915                    after,
 916                    char,
 917                    mode,
 918                    smartcase,
 919                } => {
 920                    let mut new_point =
 921                        find_forward(map, point, after, char, times, mode, smartcase);
 922                    if new_point == Some(point) {
 923                        new_point =
 924                            find_forward(map, point, after, char, times + 1, mode, smartcase);
 925                    }
 926
 927                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 928                }
 929                _ => return None,
 930            },
 931            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 932            PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
 933            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 934            EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
 935            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
 936            WindowTop => window_top(map, point, text_layout_details, times - 1),
 937            WindowMiddle => window_middle(map, point, text_layout_details),
 938            WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
 939            Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
 940            ZedSearchResult { new_selections, .. } => {
 941                // There will be only one selection, as
 942                // Search::SelectNextMatch selects a single match.
 943                if let Some(new_selection) = new_selections.first() {
 944                    (
 945                        new_selection.start.to_display_point(map),
 946                        SelectionGoal::None,
 947                    )
 948                } else {
 949                    return None;
 950                }
 951            }
 952            NextSectionStart => (
 953                section_motion(map, point, times, Direction::Next, true),
 954                SelectionGoal::None,
 955            ),
 956            NextSectionEnd => (
 957                section_motion(map, point, times, Direction::Next, false),
 958                SelectionGoal::None,
 959            ),
 960            PreviousSectionStart => (
 961                section_motion(map, point, times, Direction::Prev, true),
 962                SelectionGoal::None,
 963            ),
 964            PreviousSectionEnd => (
 965                section_motion(map, point, times, Direction::Prev, false),
 966                SelectionGoal::None,
 967            ),
 968
 969            NextMethodStart => (
 970                method_motion(map, point, times, Direction::Next, true),
 971                SelectionGoal::None,
 972            ),
 973            NextMethodEnd => (
 974                method_motion(map, point, times, Direction::Next, false),
 975                SelectionGoal::None,
 976            ),
 977            PreviousMethodStart => (
 978                method_motion(map, point, times, Direction::Prev, true),
 979                SelectionGoal::None,
 980            ),
 981            PreviousMethodEnd => (
 982                method_motion(map, point, times, Direction::Prev, false),
 983                SelectionGoal::None,
 984            ),
 985            NextComment => (
 986                comment_motion(map, point, times, Direction::Next),
 987                SelectionGoal::None,
 988            ),
 989            PreviousComment => (
 990                comment_motion(map, point, times, Direction::Prev),
 991                SelectionGoal::None,
 992            ),
 993        };
 994
 995        (new_point != point || infallible).then_some((new_point, goal))
 996    }
 997
 998    // Get the range value after self is applied to the specified selection.
 999    pub fn range(
1000        &self,
1001        map: &DisplaySnapshot,
1002        selection: Selection<DisplayPoint>,
1003        times: Option<usize>,
1004        expand_to_surrounding_newline: bool,
1005        text_layout_details: &TextLayoutDetails,
1006    ) -> Option<Range<DisplayPoint>> {
1007        if let Motion::ZedSearchResult {
1008            prior_selections,
1009            new_selections,
1010        } = self
1011        {
1012            if let Some((prior_selection, new_selection)) =
1013                prior_selections.first().zip(new_selections.first())
1014            {
1015                let start = prior_selection
1016                    .start
1017                    .to_display_point(map)
1018                    .min(new_selection.start.to_display_point(map));
1019                let end = new_selection
1020                    .end
1021                    .to_display_point(map)
1022                    .max(prior_selection.end.to_display_point(map));
1023
1024                if start < end {
1025                    return Some(start..end);
1026                } else {
1027                    return Some(end..start);
1028                }
1029            } else {
1030                return None;
1031            }
1032        }
1033
1034        if let Some((new_head, goal)) = self.move_point(
1035            map,
1036            selection.head(),
1037            selection.goal,
1038            times,
1039            text_layout_details,
1040        ) {
1041            let mut selection = selection.clone();
1042            selection.set_head(new_head, goal);
1043
1044            if self.linewise() {
1045                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1046
1047                if expand_to_surrounding_newline {
1048                    if selection.end.row() < map.max_point().row() {
1049                        *selection.end.row_mut() += 1;
1050                        *selection.end.column_mut() = 0;
1051                        selection.end = map.clip_point(selection.end, Bias::Right);
1052                        // Don't reset the end here
1053                        return Some(selection.start..selection.end);
1054                    } else if selection.start.row().0 > 0 {
1055                        *selection.start.row_mut() -= 1;
1056                        *selection.start.column_mut() = map.line_len(selection.start.row());
1057                        selection.start = map.clip_point(selection.start, Bias::Left);
1058                    }
1059                }
1060
1061                selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1062            } else {
1063                // Another special case: When using the "w" motion in combination with an
1064                // operator and the last word moved over is at the end of a line, the end of
1065                // that word becomes the end of the operated text, not the first word in the
1066                // next line.
1067                if let Motion::NextWordStart {
1068                    ignore_punctuation: _,
1069                } = self
1070                {
1071                    let start_row = MultiBufferRow(selection.start.to_point(map).row);
1072                    if selection.end.to_point(map).row > start_row.0 {
1073                        selection.end =
1074                            Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
1075                                .to_display_point(map)
1076                    }
1077                }
1078
1079                // If the motion is exclusive and the end of the motion is in column 1, the
1080                // end of the motion is moved to the end of the previous line and the motion
1081                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1082                // but "d}" will not include that line.
1083                let mut inclusive = self.inclusive();
1084                let start_point = selection.start.to_point(map);
1085                let mut end_point = selection.end.to_point(map);
1086
1087                // DisplayPoint
1088
1089                if !inclusive
1090                    && self != &Motion::Backspace
1091                    && end_point.row > start_point.row
1092                    && end_point.column == 0
1093                {
1094                    inclusive = true;
1095                    end_point.row -= 1;
1096                    end_point.column = 0;
1097                    selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1098                }
1099
1100                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
1101                    selection.end = movement::saturating_right(map, selection.end)
1102                }
1103            }
1104            Some(selection.start..selection.end)
1105        } else {
1106            None
1107        }
1108    }
1109
1110    // Expands a selection using self for an operator
1111    pub fn expand_selection(
1112        &self,
1113        map: &DisplaySnapshot,
1114        selection: &mut Selection<DisplayPoint>,
1115        times: Option<usize>,
1116        expand_to_surrounding_newline: bool,
1117        text_layout_details: &TextLayoutDetails,
1118    ) -> bool {
1119        if let Some(range) = self.range(
1120            map,
1121            selection.clone(),
1122            times,
1123            expand_to_surrounding_newline,
1124            text_layout_details,
1125        ) {
1126            selection.start = range.start;
1127            selection.end = range.end;
1128            true
1129        } else {
1130            false
1131        }
1132    }
1133}
1134
1135fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1136    for _ in 0..times {
1137        point = movement::saturating_left(map, point);
1138        if point.column() == 0 {
1139            break;
1140        }
1141    }
1142    point
1143}
1144
1145pub(crate) fn backspace(
1146    map: &DisplaySnapshot,
1147    mut point: DisplayPoint,
1148    times: usize,
1149) -> DisplayPoint {
1150    for _ in 0..times {
1151        point = movement::left(map, point);
1152        if point.is_zero() {
1153            break;
1154        }
1155    }
1156    point
1157}
1158
1159fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1160    for _ in 0..times {
1161        point = wrapping_right(map, point);
1162        if point == map.max_point() {
1163            break;
1164        }
1165    }
1166    point
1167}
1168
1169fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
1170    let max_column = map.line_len(point.row()).saturating_sub(1);
1171    if point.column() < max_column {
1172        *point.column_mut() += 1;
1173    } else if point.row() < map.max_point().row() {
1174        *point.row_mut() += 1;
1175        *point.column_mut() = 0;
1176    }
1177    point
1178}
1179
1180pub(crate) fn start_of_relative_buffer_row(
1181    map: &DisplaySnapshot,
1182    point: DisplayPoint,
1183    times: isize,
1184) -> DisplayPoint {
1185    let start = map.display_point_to_fold_point(point, Bias::Left);
1186    let target = start.row() as isize + times;
1187    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1188
1189    map.clip_point(
1190        map.fold_point_to_display_point(
1191            map.fold_snapshot
1192                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1193        ),
1194        Bias::Right,
1195    )
1196}
1197
1198fn up_down_buffer_rows(
1199    map: &DisplaySnapshot,
1200    point: DisplayPoint,
1201    mut goal: SelectionGoal,
1202    times: isize,
1203    text_layout_details: &TextLayoutDetails,
1204) -> (DisplayPoint, SelectionGoal) {
1205    let start = map.display_point_to_fold_point(point, Bias::Left);
1206    let begin_folded_line = map.fold_point_to_display_point(
1207        map.fold_snapshot
1208            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1209    );
1210    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1211
1212    let (goal_wrap, goal_x) = match goal {
1213        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1214        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1215        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1216        _ => {
1217            let x = map.x_for_display_point(point, text_layout_details);
1218            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1219            (select_nth_wrapped_row, x.0)
1220        }
1221    };
1222
1223    let target = start.row() as isize + times;
1224    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1225
1226    let mut begin_folded_line = map.fold_point_to_display_point(
1227        map.fold_snapshot
1228            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
1229    );
1230
1231    let mut i = 0;
1232    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1233        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1234        if map
1235            .display_point_to_fold_point(next_folded_line, Bias::Right)
1236            .row()
1237            == new_row
1238        {
1239            i += 1;
1240            begin_folded_line = next_folded_line;
1241        } else {
1242            break;
1243        }
1244    }
1245
1246    let new_col = if i == goal_wrap {
1247        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1248    } else {
1249        map.line_len(begin_folded_line.row())
1250    };
1251
1252    (
1253        map.clip_point(
1254            DisplayPoint::new(begin_folded_line.row(), new_col),
1255            Bias::Left,
1256        ),
1257        goal,
1258    )
1259}
1260
1261fn down_display(
1262    map: &DisplaySnapshot,
1263    mut point: DisplayPoint,
1264    mut goal: SelectionGoal,
1265    times: usize,
1266    text_layout_details: &TextLayoutDetails,
1267) -> (DisplayPoint, SelectionGoal) {
1268    for _ in 0..times {
1269        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1270    }
1271
1272    (point, goal)
1273}
1274
1275fn up_display(
1276    map: &DisplaySnapshot,
1277    mut point: DisplayPoint,
1278    mut goal: SelectionGoal,
1279    times: usize,
1280    text_layout_details: &TextLayoutDetails,
1281) -> (DisplayPoint, SelectionGoal) {
1282    for _ in 0..times {
1283        (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1284    }
1285
1286    (point, goal)
1287}
1288
1289pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1290    for _ in 0..times {
1291        let new_point = movement::saturating_right(map, point);
1292        if point == new_point {
1293            break;
1294        }
1295        point = new_point;
1296    }
1297    point
1298}
1299
1300pub(crate) fn next_char(
1301    map: &DisplaySnapshot,
1302    point: DisplayPoint,
1303    allow_cross_newline: bool,
1304) -> DisplayPoint {
1305    let mut new_point = point;
1306    let mut max_column = map.line_len(new_point.row());
1307    if !allow_cross_newline {
1308        max_column -= 1;
1309    }
1310    if new_point.column() < max_column {
1311        *new_point.column_mut() += 1;
1312    } else if new_point < map.max_point() && allow_cross_newline {
1313        *new_point.row_mut() += 1;
1314        *new_point.column_mut() = 0;
1315    }
1316    map.clip_ignoring_line_ends(new_point, Bias::Right)
1317}
1318
1319pub(crate) fn next_word_start(
1320    map: &DisplaySnapshot,
1321    mut point: DisplayPoint,
1322    ignore_punctuation: bool,
1323    times: usize,
1324) -> DisplayPoint {
1325    let classifier = map
1326        .buffer_snapshot
1327        .char_classifier_at(point.to_point(map))
1328        .ignore_punctuation(ignore_punctuation);
1329    for _ in 0..times {
1330        let mut crossed_newline = false;
1331        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1332            let left_kind = classifier.kind(left);
1333            let right_kind = classifier.kind(right);
1334            let at_newline = right == '\n';
1335
1336            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1337                || at_newline && crossed_newline
1338                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1339
1340            crossed_newline |= at_newline;
1341            found
1342        });
1343        if point == new_point {
1344            break;
1345        }
1346        point = new_point;
1347    }
1348    point
1349}
1350
1351pub(crate) fn next_word_end(
1352    map: &DisplaySnapshot,
1353    mut point: DisplayPoint,
1354    ignore_punctuation: bool,
1355    times: usize,
1356    allow_cross_newline: bool,
1357) -> DisplayPoint {
1358    let classifier = map
1359        .buffer_snapshot
1360        .char_classifier_at(point.to_point(map))
1361        .ignore_punctuation(ignore_punctuation);
1362    for _ in 0..times {
1363        let new_point = next_char(map, point, allow_cross_newline);
1364        let mut need_next_char = false;
1365        let new_point = movement::find_boundary_exclusive(
1366            map,
1367            new_point,
1368            FindRange::MultiLine,
1369            |left, right| {
1370                let left_kind = classifier.kind(left);
1371                let right_kind = classifier.kind(right);
1372                let at_newline = right == '\n';
1373
1374                if !allow_cross_newline && at_newline {
1375                    need_next_char = true;
1376                    return true;
1377                }
1378
1379                left_kind != right_kind && left_kind != CharKind::Whitespace
1380            },
1381        );
1382        let new_point = if need_next_char {
1383            next_char(map, new_point, true)
1384        } else {
1385            new_point
1386        };
1387        let new_point = map.clip_point(new_point, Bias::Left);
1388        if point == new_point {
1389            break;
1390        }
1391        point = new_point;
1392    }
1393    point
1394}
1395
1396fn previous_word_start(
1397    map: &DisplaySnapshot,
1398    mut point: DisplayPoint,
1399    ignore_punctuation: bool,
1400    times: usize,
1401) -> DisplayPoint {
1402    let classifier = map
1403        .buffer_snapshot
1404        .char_classifier_at(point.to_point(map))
1405        .ignore_punctuation(ignore_punctuation);
1406    for _ in 0..times {
1407        // This works even though find_preceding_boundary is called for every character in the line containing
1408        // cursor because the newline is checked only once.
1409        let new_point = movement::find_preceding_boundary_display_point(
1410            map,
1411            point,
1412            FindRange::MultiLine,
1413            |left, right| {
1414                let left_kind = classifier.kind(left);
1415                let right_kind = classifier.kind(right);
1416
1417                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1418            },
1419        );
1420        if point == new_point {
1421            break;
1422        }
1423        point = new_point;
1424    }
1425    point
1426}
1427
1428fn previous_word_end(
1429    map: &DisplaySnapshot,
1430    point: DisplayPoint,
1431    ignore_punctuation: bool,
1432    times: usize,
1433) -> DisplayPoint {
1434    let classifier = map
1435        .buffer_snapshot
1436        .char_classifier_at(point.to_point(map))
1437        .ignore_punctuation(ignore_punctuation);
1438    let mut point = point.to_point(map);
1439
1440    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1441        point.column += 1;
1442    }
1443    for _ in 0..times {
1444        let new_point = movement::find_preceding_boundary_point(
1445            &map.buffer_snapshot,
1446            point,
1447            FindRange::MultiLine,
1448            |left, right| {
1449                let left_kind = classifier.kind(left);
1450                let right_kind = classifier.kind(right);
1451                match (left_kind, right_kind) {
1452                    (CharKind::Punctuation, CharKind::Whitespace)
1453                    | (CharKind::Punctuation, CharKind::Word)
1454                    | (CharKind::Word, CharKind::Whitespace)
1455                    | (CharKind::Word, CharKind::Punctuation) => true,
1456                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1457                    _ => false,
1458                }
1459            },
1460        );
1461        if new_point == point {
1462            break;
1463        }
1464        point = new_point;
1465    }
1466    movement::saturating_left(map, point.to_display_point(map))
1467}
1468
1469fn next_subword_start(
1470    map: &DisplaySnapshot,
1471    mut point: DisplayPoint,
1472    ignore_punctuation: bool,
1473    times: usize,
1474) -> DisplayPoint {
1475    let classifier = map
1476        .buffer_snapshot
1477        .char_classifier_at(point.to_point(map))
1478        .ignore_punctuation(ignore_punctuation);
1479    for _ in 0..times {
1480        let mut crossed_newline = false;
1481        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1482            let left_kind = classifier.kind(left);
1483            let right_kind = classifier.kind(right);
1484            let at_newline = right == '\n';
1485
1486            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1487            let is_subword_start =
1488                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1489
1490            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1491                || at_newline && crossed_newline
1492                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1493
1494            crossed_newline |= at_newline;
1495            found
1496        });
1497        if point == new_point {
1498            break;
1499        }
1500        point = new_point;
1501    }
1502    point
1503}
1504
1505pub(crate) fn next_subword_end(
1506    map: &DisplaySnapshot,
1507    mut point: DisplayPoint,
1508    ignore_punctuation: bool,
1509    times: usize,
1510    allow_cross_newline: bool,
1511) -> DisplayPoint {
1512    let classifier = map
1513        .buffer_snapshot
1514        .char_classifier_at(point.to_point(map))
1515        .ignore_punctuation(ignore_punctuation);
1516    for _ in 0..times {
1517        let new_point = next_char(map, point, allow_cross_newline);
1518
1519        let mut crossed_newline = false;
1520        let mut need_backtrack = false;
1521        let new_point =
1522            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1523                let left_kind = classifier.kind(left);
1524                let right_kind = classifier.kind(right);
1525                let at_newline = right == '\n';
1526
1527                if !allow_cross_newline && at_newline {
1528                    return true;
1529                }
1530
1531                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1532                let is_subword_end =
1533                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1534
1535                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1536
1537                if found && (is_word_end || is_subword_end) {
1538                    need_backtrack = true;
1539                }
1540
1541                crossed_newline |= at_newline;
1542                found
1543            });
1544        let mut new_point = map.clip_point(new_point, Bias::Left);
1545        if need_backtrack {
1546            *new_point.column_mut() -= 1;
1547        }
1548        if point == new_point {
1549            break;
1550        }
1551        point = new_point;
1552    }
1553    point
1554}
1555
1556fn previous_subword_start(
1557    map: &DisplaySnapshot,
1558    mut point: DisplayPoint,
1559    ignore_punctuation: bool,
1560    times: usize,
1561) -> DisplayPoint {
1562    let classifier = map
1563        .buffer_snapshot
1564        .char_classifier_at(point.to_point(map))
1565        .ignore_punctuation(ignore_punctuation);
1566    for _ in 0..times {
1567        let mut crossed_newline = false;
1568        // This works even though find_preceding_boundary is called for every character in the line containing
1569        // cursor because the newline is checked only once.
1570        let new_point = movement::find_preceding_boundary_display_point(
1571            map,
1572            point,
1573            FindRange::MultiLine,
1574            |left, right| {
1575                let left_kind = classifier.kind(left);
1576                let right_kind = classifier.kind(right);
1577                let at_newline = right == '\n';
1578
1579                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1580                let is_subword_start =
1581                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1582
1583                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1584                    || at_newline && crossed_newline
1585                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1586
1587                crossed_newline |= at_newline;
1588
1589                found
1590            },
1591        );
1592        if point == new_point {
1593            break;
1594        }
1595        point = new_point;
1596    }
1597    point
1598}
1599
1600fn previous_subword_end(
1601    map: &DisplaySnapshot,
1602    point: DisplayPoint,
1603    ignore_punctuation: bool,
1604    times: usize,
1605) -> DisplayPoint {
1606    let classifier = map
1607        .buffer_snapshot
1608        .char_classifier_at(point.to_point(map))
1609        .ignore_punctuation(ignore_punctuation);
1610    let mut point = point.to_point(map);
1611
1612    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1613        point.column += 1;
1614    }
1615    for _ in 0..times {
1616        let new_point = movement::find_preceding_boundary_point(
1617            &map.buffer_snapshot,
1618            point,
1619            FindRange::MultiLine,
1620            |left, right| {
1621                let left_kind = classifier.kind(left);
1622                let right_kind = classifier.kind(right);
1623
1624                let is_subword_end =
1625                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1626
1627                if is_subword_end {
1628                    return true;
1629                }
1630
1631                match (left_kind, right_kind) {
1632                    (CharKind::Word, CharKind::Whitespace)
1633                    | (CharKind::Word, CharKind::Punctuation) => true,
1634                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1635                    _ => false,
1636                }
1637            },
1638        );
1639        if new_point == point {
1640            break;
1641        }
1642        point = new_point;
1643    }
1644    movement::saturating_left(map, point.to_display_point(map))
1645}
1646
1647pub(crate) fn first_non_whitespace(
1648    map: &DisplaySnapshot,
1649    display_lines: bool,
1650    from: DisplayPoint,
1651) -> DisplayPoint {
1652    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1653    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1654    for (ch, offset) in map.buffer_chars_at(start_offset) {
1655        if ch == '\n' {
1656            return from;
1657        }
1658
1659        start_offset = offset;
1660
1661        if classifier.kind(ch) != CharKind::Whitespace {
1662            break;
1663        }
1664    }
1665
1666    start_offset.to_display_point(map)
1667}
1668
1669pub(crate) fn last_non_whitespace(
1670    map: &DisplaySnapshot,
1671    from: DisplayPoint,
1672    count: usize,
1673) -> DisplayPoint {
1674    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1675    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1676
1677    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1678    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1679        if classifier.kind(ch) != CharKind::Whitespace {
1680            return end_of_line.to_display_point(map);
1681        }
1682    }
1683
1684    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1685        if ch == '\n' {
1686            break;
1687        }
1688        end_of_line = offset;
1689        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1690            break;
1691        }
1692    }
1693
1694    end_of_line.to_display_point(map)
1695}
1696
1697pub(crate) fn start_of_line(
1698    map: &DisplaySnapshot,
1699    display_lines: bool,
1700    point: DisplayPoint,
1701) -> DisplayPoint {
1702    if display_lines {
1703        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1704    } else {
1705        map.prev_line_boundary(point.to_point(map)).1
1706    }
1707}
1708
1709pub(crate) fn end_of_line(
1710    map: &DisplaySnapshot,
1711    display_lines: bool,
1712    mut point: DisplayPoint,
1713    times: usize,
1714) -> DisplayPoint {
1715    if times > 1 {
1716        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1717    }
1718    if display_lines {
1719        map.clip_point(
1720            DisplayPoint::new(point.row(), map.line_len(point.row())),
1721            Bias::Left,
1722        )
1723    } else {
1724        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1725    }
1726}
1727
1728fn sentence_backwards(
1729    map: &DisplaySnapshot,
1730    point: DisplayPoint,
1731    mut times: usize,
1732) -> DisplayPoint {
1733    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1734    let mut chars = map.reverse_buffer_chars_at(start).peekable();
1735
1736    let mut was_newline = map
1737        .buffer_chars_at(start)
1738        .next()
1739        .is_some_and(|(c, _)| c == '\n');
1740
1741    while let Some((ch, offset)) = chars.next() {
1742        let start_of_next_sentence = if was_newline && ch == '\n' {
1743            Some(offset + ch.len_utf8())
1744        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1745            Some(next_non_blank(map, offset + ch.len_utf8()))
1746        } else if ch == '.' || ch == '?' || ch == '!' {
1747            start_of_next_sentence(map, offset + ch.len_utf8())
1748        } else {
1749            None
1750        };
1751
1752        if let Some(start_of_next_sentence) = start_of_next_sentence {
1753            if start_of_next_sentence < start {
1754                times = times.saturating_sub(1);
1755            }
1756            if times == 0 || offset == 0 {
1757                return map.clip_point(
1758                    start_of_next_sentence
1759                        .to_offset(&map.buffer_snapshot)
1760                        .to_display_point(map),
1761                    Bias::Left,
1762                );
1763            }
1764        }
1765        if was_newline {
1766            start = offset;
1767        }
1768        was_newline = ch == '\n';
1769    }
1770
1771    DisplayPoint::zero()
1772}
1773
1774fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1775    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1776    let mut chars = map.buffer_chars_at(start).peekable();
1777
1778    let mut was_newline = map
1779        .reverse_buffer_chars_at(start)
1780        .next()
1781        .is_some_and(|(c, _)| c == '\n')
1782        && chars.peek().is_some_and(|(c, _)| *c == '\n');
1783
1784    while let Some((ch, offset)) = chars.next() {
1785        if was_newline && ch == '\n' {
1786            continue;
1787        }
1788        let start_of_next_sentence = if was_newline {
1789            Some(next_non_blank(map, offset))
1790        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1791            Some(next_non_blank(map, offset + ch.len_utf8()))
1792        } else if ch == '.' || ch == '?' || ch == '!' {
1793            start_of_next_sentence(map, offset + ch.len_utf8())
1794        } else {
1795            None
1796        };
1797
1798        if let Some(start_of_next_sentence) = start_of_next_sentence {
1799            times = times.saturating_sub(1);
1800            if times == 0 {
1801                return map.clip_point(
1802                    start_of_next_sentence
1803                        .to_offset(&map.buffer_snapshot)
1804                        .to_display_point(map),
1805                    Bias::Right,
1806                );
1807            }
1808        }
1809
1810        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1811    }
1812
1813    map.max_point()
1814}
1815
1816fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1817    for (c, o) in map.buffer_chars_at(start) {
1818        if c == '\n' || !c.is_whitespace() {
1819            return o;
1820        }
1821    }
1822
1823    map.buffer_snapshot.len()
1824}
1825
1826// given the offset after a ., !, or ? find the start of the next sentence.
1827// if this is not a sentence boundary, returns None.
1828fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1829    let chars = map.buffer_chars_at(end_of_sentence);
1830    let mut seen_space = false;
1831
1832    for (char, offset) in chars {
1833        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1834            continue;
1835        }
1836
1837        if char == '\n' && seen_space {
1838            return Some(offset);
1839        } else if char.is_whitespace() {
1840            seen_space = true;
1841        } else if seen_space {
1842            return Some(offset);
1843        } else {
1844            return None;
1845        }
1846    }
1847
1848    Some(map.buffer_snapshot.len())
1849}
1850
1851fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1852    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1853    *new_point.column_mut() = point.column();
1854    map.clip_point(new_point, Bias::Left)
1855}
1856
1857fn end_of_document(
1858    map: &DisplaySnapshot,
1859    point: DisplayPoint,
1860    line: Option<usize>,
1861) -> DisplayPoint {
1862    let new_row = if let Some(line) = line {
1863        (line - 1) as u32
1864    } else {
1865        map.max_buffer_row().0
1866    };
1867
1868    let new_point = Point::new(new_row, point.column());
1869    map.clip_point(new_point.to_display_point(map), Bias::Left)
1870}
1871
1872fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
1873    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
1874    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
1875
1876    if head > outer.start && head < inner.start {
1877        let mut offset = inner.end.to_offset(map, Bias::Left);
1878        for c in map.buffer_snapshot.chars_at(offset) {
1879            if c == '/' || c == '\n' || c == '>' {
1880                return Some(offset.to_display_point(map));
1881            }
1882            offset += c.len_utf8();
1883        }
1884    } else {
1885        let mut offset = outer.start.to_offset(map, Bias::Left);
1886        for c in map.buffer_snapshot.chars_at(offset) {
1887            offset += c.len_utf8();
1888            if c == '<' || c == '\n' {
1889                return Some(offset.to_display_point(map));
1890            }
1891        }
1892    }
1893
1894    return None;
1895}
1896
1897fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1898    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1899    let display_point = map.clip_at_line_end(display_point);
1900    let point = display_point.to_point(map);
1901    let offset = point.to_offset(&map.buffer_snapshot);
1902
1903    // Ensure the range is contained by the current line.
1904    let mut line_end = map.next_line_boundary(point).0;
1905    if line_end == point {
1906        line_end = map.max_point().to_point(map);
1907    }
1908
1909    let line_range = map.prev_line_boundary(point).0..line_end;
1910    let visible_line_range =
1911        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1912    let ranges = map
1913        .buffer_snapshot
1914        .bracket_ranges(visible_line_range.clone());
1915    if let Some(ranges) = ranges {
1916        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1917            ..line_range.end.to_offset(&map.buffer_snapshot);
1918        let mut closest_pair_destination = None;
1919        let mut closest_distance = usize::MAX;
1920
1921        for (open_range, close_range) in ranges {
1922            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
1923                if offset > open_range.start && offset < close_range.start {
1924                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1925                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
1926                        return display_point;
1927                    }
1928                    if let Some(tag) = matching_tag(map, display_point) {
1929                        return tag;
1930                    }
1931                } else if close_range.contains(&offset) {
1932                    return open_range.start.to_display_point(map);
1933                } else if open_range.contains(&offset) {
1934                    return (close_range.end - 1).to_display_point(map);
1935                }
1936            }
1937
1938            if open_range.start >= offset && line_range.contains(&open_range.start) {
1939                let distance = open_range.start - offset;
1940                if distance < closest_distance {
1941                    closest_pair_destination = Some(close_range.end - 1);
1942                    closest_distance = distance;
1943                    continue;
1944                }
1945            }
1946
1947            if close_range.start >= offset && line_range.contains(&close_range.start) {
1948                let distance = close_range.start - offset;
1949                if distance < closest_distance {
1950                    closest_pair_destination = Some(open_range.start);
1951                    closest_distance = distance;
1952                    continue;
1953                }
1954            }
1955
1956            continue;
1957        }
1958
1959        closest_pair_destination
1960            .map(|destination| destination.to_display_point(map))
1961            .unwrap_or(display_point)
1962    } else {
1963        display_point
1964    }
1965}
1966
1967fn unmatched_forward(
1968    map: &DisplaySnapshot,
1969    mut display_point: DisplayPoint,
1970    char: char,
1971    times: usize,
1972) -> DisplayPoint {
1973    for _ in 0..times {
1974        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
1975        let point = display_point.to_point(map);
1976        let offset = point.to_offset(&map.buffer_snapshot);
1977
1978        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
1979        let Some(ranges) = ranges else { break };
1980        let mut closest_closing_destination = None;
1981        let mut closest_distance = usize::MAX;
1982
1983        for (_, close_range) in ranges {
1984            if close_range.start > offset {
1985                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1986                if Some(char) == chars.next() {
1987                    let distance = close_range.start - offset;
1988                    if distance < closest_distance {
1989                        closest_closing_destination = Some(close_range.start);
1990                        closest_distance = distance;
1991                        continue;
1992                    }
1993                }
1994            }
1995        }
1996
1997        let new_point = closest_closing_destination
1998            .map(|destination| destination.to_display_point(map))
1999            .unwrap_or(display_point);
2000        if new_point == display_point {
2001            break;
2002        }
2003        display_point = new_point;
2004    }
2005    return display_point;
2006}
2007
2008fn unmatched_backward(
2009    map: &DisplaySnapshot,
2010    mut display_point: DisplayPoint,
2011    char: char,
2012    times: usize,
2013) -> DisplayPoint {
2014    for _ in 0..times {
2015        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2016        let point = display_point.to_point(map);
2017        let offset = point.to_offset(&map.buffer_snapshot);
2018
2019        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2020        let Some(ranges) = ranges else {
2021            break;
2022        };
2023
2024        let mut closest_starting_destination = None;
2025        let mut closest_distance = usize::MAX;
2026
2027        for (start_range, _) in ranges {
2028            if start_range.start < offset {
2029                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2030                if Some(char) == chars.next() {
2031                    let distance = offset - start_range.start;
2032                    if distance < closest_distance {
2033                        closest_starting_destination = Some(start_range.start);
2034                        closest_distance = distance;
2035                        continue;
2036                    }
2037                }
2038            }
2039        }
2040
2041        let new_point = closest_starting_destination
2042            .map(|destination| destination.to_display_point(map))
2043            .unwrap_or(display_point);
2044        if new_point == display_point {
2045            break;
2046        } else {
2047            display_point = new_point;
2048        }
2049    }
2050    display_point
2051}
2052
2053fn find_forward(
2054    map: &DisplaySnapshot,
2055    from: DisplayPoint,
2056    before: bool,
2057    target: char,
2058    times: usize,
2059    mode: FindRange,
2060    smartcase: bool,
2061) -> Option<DisplayPoint> {
2062    let mut to = from;
2063    let mut found = false;
2064
2065    for _ in 0..times {
2066        found = false;
2067        let new_to = find_boundary(map, to, mode, |_, right| {
2068            found = is_character_match(target, right, smartcase);
2069            found
2070        });
2071        if to == new_to {
2072            break;
2073        }
2074        to = new_to;
2075    }
2076
2077    if found {
2078        if before && to.column() > 0 {
2079            *to.column_mut() -= 1;
2080            Some(map.clip_point(to, Bias::Left))
2081        } else {
2082            Some(to)
2083        }
2084    } else {
2085        None
2086    }
2087}
2088
2089fn find_backward(
2090    map: &DisplaySnapshot,
2091    from: DisplayPoint,
2092    after: bool,
2093    target: char,
2094    times: usize,
2095    mode: FindRange,
2096    smartcase: bool,
2097) -> DisplayPoint {
2098    let mut to = from;
2099
2100    for _ in 0..times {
2101        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2102            is_character_match(target, right, smartcase)
2103        });
2104        if to == new_to {
2105            break;
2106        }
2107        to = new_to;
2108    }
2109
2110    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2111    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2112        if after {
2113            *to.column_mut() += 1;
2114            map.clip_point(to, Bias::Right)
2115        } else {
2116            to
2117        }
2118    } else {
2119        from
2120    }
2121}
2122
2123fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2124    if smartcase {
2125        if target.is_uppercase() {
2126            target == other
2127        } else {
2128            target == other.to_ascii_lowercase()
2129        }
2130    } else {
2131        target == other
2132    }
2133}
2134
2135fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2136    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2137    first_non_whitespace(map, false, correct_line)
2138}
2139
2140fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2141    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2142    first_non_whitespace(map, false, correct_line)
2143}
2144
2145fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2146    let correct_line = start_of_relative_buffer_row(map, point, 0);
2147    right(map, correct_line, times.saturating_sub(1))
2148}
2149
2150pub(crate) fn next_line_end(
2151    map: &DisplaySnapshot,
2152    mut point: DisplayPoint,
2153    times: usize,
2154) -> DisplayPoint {
2155    if times > 1 {
2156        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2157    }
2158    end_of_line(map, false, point, 1)
2159}
2160
2161fn window_top(
2162    map: &DisplaySnapshot,
2163    point: DisplayPoint,
2164    text_layout_details: &TextLayoutDetails,
2165    mut times: usize,
2166) -> (DisplayPoint, SelectionGoal) {
2167    let first_visible_line = text_layout_details
2168        .scroll_anchor
2169        .anchor
2170        .to_display_point(map);
2171
2172    if first_visible_line.row() != DisplayRow(0)
2173        && text_layout_details.vertical_scroll_margin as usize > times
2174    {
2175        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2176    }
2177
2178    if let Some(visible_rows) = text_layout_details.visible_rows {
2179        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2180        let new_row = (first_visible_line.row().0 + (times as u32))
2181            .min(bottom_row)
2182            .min(map.max_point().row().0);
2183        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2184
2185        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2186        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2187    } else {
2188        let new_row =
2189            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2190        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2191
2192        let new_point = DisplayPoint::new(new_row, new_col);
2193        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2194    }
2195}
2196
2197fn window_middle(
2198    map: &DisplaySnapshot,
2199    point: DisplayPoint,
2200    text_layout_details: &TextLayoutDetails,
2201) -> (DisplayPoint, SelectionGoal) {
2202    if let Some(visible_rows) = text_layout_details.visible_rows {
2203        let first_visible_line = text_layout_details
2204            .scroll_anchor
2205            .anchor
2206            .to_display_point(map);
2207
2208        let max_visible_rows =
2209            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2210
2211        let new_row =
2212            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2213        let new_row = DisplayRow(new_row);
2214        let new_col = point.column().min(map.line_len(new_row));
2215        let new_point = DisplayPoint::new(new_row, new_col);
2216        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2217    } else {
2218        (point, SelectionGoal::None)
2219    }
2220}
2221
2222fn window_bottom(
2223    map: &DisplaySnapshot,
2224    point: DisplayPoint,
2225    text_layout_details: &TextLayoutDetails,
2226    mut times: usize,
2227) -> (DisplayPoint, SelectionGoal) {
2228    if let Some(visible_rows) = text_layout_details.visible_rows {
2229        let first_visible_line = text_layout_details
2230            .scroll_anchor
2231            .anchor
2232            .to_display_point(map);
2233        let bottom_row = first_visible_line.row().0
2234            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2235        if bottom_row < map.max_point().row().0
2236            && text_layout_details.vertical_scroll_margin as usize > times
2237        {
2238            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2239        }
2240        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2241        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2242        {
2243            first_visible_line.row()
2244        } else {
2245            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2246        };
2247        let new_col = point.column().min(map.line_len(new_row));
2248        let new_point = DisplayPoint::new(new_row, new_col);
2249        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2250    } else {
2251        (point, SelectionGoal::None)
2252    }
2253}
2254
2255fn method_motion(
2256    map: &DisplaySnapshot,
2257    mut display_point: DisplayPoint,
2258    times: usize,
2259    direction: Direction,
2260    is_start: bool,
2261) -> DisplayPoint {
2262    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2263        return display_point;
2264    };
2265
2266    for _ in 0..times {
2267        let point = map.display_point_to_point(display_point, Bias::Left);
2268        let offset = point.to_offset(&map.buffer_snapshot);
2269        let range = if direction == Direction::Prev {
2270            0..offset
2271        } else {
2272            offset..buffer.len()
2273        };
2274
2275        let possibilities = buffer
2276            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2277            .filter_map(|(range, object)| {
2278                if !matches!(object, language::TextObject::AroundFunction) {
2279                    return None;
2280                }
2281
2282                let relevant = if is_start { range.start } else { range.end };
2283                if direction == Direction::Prev && relevant < offset {
2284                    Some(relevant)
2285                } else if direction == Direction::Next && relevant > offset + 1 {
2286                    Some(relevant)
2287                } else {
2288                    None
2289                }
2290            });
2291
2292        let dest = if direction == Direction::Prev {
2293            possibilities.max().unwrap_or(offset)
2294        } else {
2295            possibilities.min().unwrap_or(offset)
2296        };
2297        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2298        if new_point == display_point {
2299            break;
2300        }
2301        display_point = new_point;
2302    }
2303    display_point
2304}
2305
2306fn comment_motion(
2307    map: &DisplaySnapshot,
2308    mut display_point: DisplayPoint,
2309    times: usize,
2310    direction: Direction,
2311) -> DisplayPoint {
2312    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2313        return display_point;
2314    };
2315
2316    for _ in 0..times {
2317        let point = map.display_point_to_point(display_point, Bias::Left);
2318        let offset = point.to_offset(&map.buffer_snapshot);
2319        let range = if direction == Direction::Prev {
2320            0..offset
2321        } else {
2322            offset..buffer.len()
2323        };
2324
2325        let possibilities = buffer
2326            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2327            .filter_map(|(range, object)| {
2328                if !matches!(object, language::TextObject::AroundComment) {
2329                    return None;
2330                }
2331
2332                let relevant = if direction == Direction::Prev {
2333                    range.start
2334                } else {
2335                    range.end
2336                };
2337                if direction == Direction::Prev && relevant < offset {
2338                    Some(relevant)
2339                } else if direction == Direction::Next && relevant > offset + 1 {
2340                    Some(relevant)
2341                } else {
2342                    None
2343                }
2344            });
2345
2346        let dest = if direction == Direction::Prev {
2347            possibilities.max().unwrap_or(offset)
2348        } else {
2349            possibilities.min().unwrap_or(offset)
2350        };
2351        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2352        if new_point == display_point {
2353            break;
2354        }
2355        display_point = new_point;
2356    }
2357
2358    display_point
2359}
2360
2361fn section_motion(
2362    map: &DisplaySnapshot,
2363    mut display_point: DisplayPoint,
2364    times: usize,
2365    direction: Direction,
2366    is_start: bool,
2367) -> DisplayPoint {
2368    if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
2369        for _ in 0..times {
2370            let offset = map
2371                .display_point_to_point(display_point, Bias::Left)
2372                .to_offset(&map.buffer_snapshot);
2373            let range = if direction == Direction::Prev {
2374                0..offset
2375            } else {
2376                offset..buffer.len()
2377            };
2378
2379            // we set a max start depth here because we want a section to only be "top level"
2380            // similar to vim's default of '{' in the first column.
2381            // (and without it, ]] at the start of editor.rs is -very- slow)
2382            let mut possibilities = buffer
2383                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2384                .filter(|(_, object)| {
2385                    matches!(
2386                        object,
2387                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2388                    )
2389                })
2390                .collect::<Vec<_>>();
2391            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2392            let mut prev_end = None;
2393            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2394                if t == language::TextObject::AroundFunction
2395                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2396                {
2397                    return None;
2398                }
2399                prev_end = Some(range.end);
2400
2401                let relevant = if is_start { range.start } else { range.end };
2402                if direction == Direction::Prev && relevant < offset {
2403                    Some(relevant)
2404                } else if direction == Direction::Next && relevant > offset + 1 {
2405                    Some(relevant)
2406                } else {
2407                    None
2408                }
2409            });
2410
2411            let offset = if direction == Direction::Prev {
2412                possibilities.max().unwrap_or(0)
2413            } else {
2414                possibilities.min().unwrap_or(buffer.len())
2415            };
2416
2417            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2418            if new_point == display_point {
2419                break;
2420            }
2421            display_point = new_point;
2422        }
2423        return display_point;
2424    };
2425
2426    for _ in 0..times {
2427        let point = map.display_point_to_point(display_point, Bias::Left);
2428        let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2429            return display_point;
2430        };
2431        let next_point = match (direction, is_start) {
2432            (Direction::Prev, true) => {
2433                let mut start = excerpt.start_anchor().to_display_point(&map);
2434                if start >= display_point && start.row() > DisplayRow(0) {
2435                    let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2436                        return display_point;
2437                    };
2438                    start = excerpt.start_anchor().to_display_point(&map);
2439                }
2440                start
2441            }
2442            (Direction::Prev, false) => {
2443                let mut start = excerpt.start_anchor().to_display_point(&map);
2444                if start.row() > DisplayRow(0) {
2445                    *start.row_mut() -= 1;
2446                }
2447                map.clip_point(start, Bias::Left)
2448            }
2449            (Direction::Next, true) => {
2450                let mut end = excerpt.end_anchor().to_display_point(&map);
2451                *end.row_mut() += 1;
2452                map.clip_point(end, Bias::Right)
2453            }
2454            (Direction::Next, false) => {
2455                let mut end = excerpt.end_anchor().to_display_point(&map);
2456                *end.column_mut() = 0;
2457                if end <= display_point {
2458                    *end.row_mut() += 1;
2459                    let point_end = map.display_point_to_point(end, Bias::Right);
2460                    let Some(excerpt) =
2461                        map.buffer_snapshot.excerpt_containing(point_end..point_end)
2462                    else {
2463                        return display_point;
2464                    };
2465                    end = excerpt.end_anchor().to_display_point(&map);
2466                    *end.column_mut() = 0;
2467                }
2468                end
2469            }
2470        };
2471        if next_point == display_point {
2472            break;
2473        }
2474        display_point = next_point;
2475    }
2476
2477    display_point
2478}
2479
2480#[cfg(test)]
2481mod test {
2482
2483    use crate::test::NeovimBackedTestContext;
2484    use indoc::indoc;
2485
2486    #[gpui::test]
2487    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2488        let mut cx = NeovimBackedTestContext::new(cx).await;
2489
2490        let initial_state = indoc! {r"ˇabc
2491            def
2492
2493            paragraph
2494            the second
2495
2496
2497
2498            third and
2499            final"};
2500
2501        // goes down once
2502        cx.set_shared_state(initial_state).await;
2503        cx.simulate_shared_keystrokes("}").await;
2504        cx.shared_state().await.assert_eq(indoc! {r"abc
2505            def
2506            ˇ
2507            paragraph
2508            the second
2509
2510
2511
2512            third and
2513            final"});
2514
2515        // goes up once
2516        cx.simulate_shared_keystrokes("{").await;
2517        cx.shared_state().await.assert_eq(initial_state);
2518
2519        // goes down twice
2520        cx.simulate_shared_keystrokes("2 }").await;
2521        cx.shared_state().await.assert_eq(indoc! {r"abc
2522            def
2523
2524            paragraph
2525            the second
2526            ˇ
2527
2528
2529            third and
2530            final"});
2531
2532        // goes down over multiple blanks
2533        cx.simulate_shared_keystrokes("}").await;
2534        cx.shared_state().await.assert_eq(indoc! {r"abc
2535                def
2536
2537                paragraph
2538                the second
2539
2540
2541
2542                third and
2543                finaˇl"});
2544
2545        // goes up twice
2546        cx.simulate_shared_keystrokes("2 {").await;
2547        cx.shared_state().await.assert_eq(indoc! {r"abc
2548                def
2549                ˇ
2550                paragraph
2551                the second
2552
2553
2554
2555                third and
2556                final"});
2557    }
2558
2559    #[gpui::test]
2560    async fn test_matching(cx: &mut gpui::TestAppContext) {
2561        let mut cx = NeovimBackedTestContext::new(cx).await;
2562
2563        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2564                do(something(with<Types>.and_arrays[0, 2]))
2565            }"})
2566            .await;
2567        cx.simulate_shared_keystrokes("%").await;
2568        cx.shared_state()
2569            .await
2570            .assert_eq(indoc! {r"func (a stringˇ) {
2571                do(something(with<Types>.and_arrays[0, 2]))
2572            }"});
2573
2574        // test it works on the last character of the line
2575        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2576            do(something(with<Types>.and_arrays[0, 2]))
2577            }"})
2578            .await;
2579        cx.simulate_shared_keystrokes("%").await;
2580        cx.shared_state()
2581            .await
2582            .assert_eq(indoc! {r"func (a string) {
2583            do(something(with<Types>.and_arrays[0, 2]))
2584            ˇ}"});
2585
2586        // test it works on immediate nesting
2587        cx.set_shared_state("ˇ{()}").await;
2588        cx.simulate_shared_keystrokes("%").await;
2589        cx.shared_state().await.assert_eq("{()ˇ}");
2590        cx.simulate_shared_keystrokes("%").await;
2591        cx.shared_state().await.assert_eq("ˇ{()}");
2592
2593        // test it works on immediate nesting inside braces
2594        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2595        cx.simulate_shared_keystrokes("%").await;
2596        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2597
2598        // test it jumps to the next paren on a line
2599        cx.set_shared_state("func ˇboop() {\n}").await;
2600        cx.simulate_shared_keystrokes("%").await;
2601        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2602    }
2603
2604    #[gpui::test]
2605    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2606        let mut cx = NeovimBackedTestContext::new(cx).await;
2607
2608        // test it works with curly braces
2609        cx.set_shared_state(indoc! {r"func (a string) {
2610                do(something(with<Types>.anˇd_arrays[0, 2]))
2611            }"})
2612            .await;
2613        cx.simulate_shared_keystrokes("] }").await;
2614        cx.shared_state()
2615            .await
2616            .assert_eq(indoc! {r"func (a string) {
2617                do(something(with<Types>.and_arrays[0, 2]))
2618            ˇ}"});
2619
2620        // test it works with brackets
2621        cx.set_shared_state(indoc! {r"func (a string) {
2622                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2623            }"})
2624            .await;
2625        cx.simulate_shared_keystrokes("] )").await;
2626        cx.shared_state()
2627            .await
2628            .assert_eq(indoc! {r"func (a string) {
2629                do(something(with<Types>.and_arrays[0, 2])ˇ)
2630            }"});
2631
2632        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2633            .await;
2634        cx.simulate_shared_keystrokes("] )").await;
2635        cx.shared_state()
2636            .await
2637            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2638
2639        // test it works on immediate nesting
2640        cx.set_shared_state("{ˇ {}{}}").await;
2641        cx.simulate_shared_keystrokes("] }").await;
2642        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2643        cx.set_shared_state("(ˇ ()())").await;
2644        cx.simulate_shared_keystrokes("] )").await;
2645        cx.shared_state().await.assert_eq("( ()()ˇ)");
2646
2647        // test it works on immediate nesting inside braces
2648        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2649        cx.simulate_shared_keystrokes("] }").await;
2650        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2651        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2652        cx.simulate_shared_keystrokes("] )").await;
2653        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2654    }
2655
2656    #[gpui::test]
2657    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2658        let mut cx = NeovimBackedTestContext::new(cx).await;
2659
2660        // test it works with curly braces
2661        cx.set_shared_state(indoc! {r"func (a string) {
2662                do(something(with<Types>.anˇd_arrays[0, 2]))
2663            }"})
2664            .await;
2665        cx.simulate_shared_keystrokes("[ {").await;
2666        cx.shared_state()
2667            .await
2668            .assert_eq(indoc! {r"func (a string) ˇ{
2669                do(something(with<Types>.and_arrays[0, 2]))
2670            }"});
2671
2672        // test it works with brackets
2673        cx.set_shared_state(indoc! {r"func (a string) {
2674                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2675            }"})
2676            .await;
2677        cx.simulate_shared_keystrokes("[ (").await;
2678        cx.shared_state()
2679            .await
2680            .assert_eq(indoc! {r"func (a string) {
2681                doˇ(something(with<Types>.and_arrays[0, 2]))
2682            }"});
2683
2684        // test it works on immediate nesting
2685        cx.set_shared_state("{{}{} ˇ }").await;
2686        cx.simulate_shared_keystrokes("[ {").await;
2687        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2688        cx.set_shared_state("(()() ˇ )").await;
2689        cx.simulate_shared_keystrokes("[ (").await;
2690        cx.shared_state().await.assert_eq("ˇ(()()  )");
2691
2692        // test it works on immediate nesting inside braces
2693        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2694        cx.simulate_shared_keystrokes("[ {").await;
2695        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2696        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2697        cx.simulate_shared_keystrokes("[ (").await;
2698        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2699    }
2700
2701    #[gpui::test]
2702    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2703        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2704
2705        cx.neovim.exec("set filetype=html").await;
2706
2707        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2708        cx.simulate_shared_keystrokes("%").await;
2709        cx.shared_state()
2710            .await
2711            .assert_eq(indoc! {r"<body><ˇ/body>"});
2712        cx.simulate_shared_keystrokes("%").await;
2713
2714        // test jumping backwards
2715        cx.shared_state()
2716            .await
2717            .assert_eq(indoc! {r"<ˇbody></body>"});
2718
2719        // test self-closing tags
2720        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2721        cx.simulate_shared_keystrokes("%").await;
2722        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2723
2724        // test tag with attributes
2725        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2726            </div>
2727            "})
2728            .await;
2729        cx.simulate_shared_keystrokes("%").await;
2730        cx.shared_state()
2731            .await
2732            .assert_eq(indoc! {r"<div class='test' id='main'>
2733            <ˇ/div>
2734            "});
2735
2736        // test multi-line self-closing tag
2737        cx.set_shared_state(indoc! {r#"<a>
2738            <br
2739                test = "test"
2740            /ˇ>
2741        </a>"#})
2742            .await;
2743        cx.simulate_shared_keystrokes("%").await;
2744        cx.shared_state().await.assert_eq(indoc! {r#"<a>
2745            ˇ<br
2746                test = "test"
2747            />
2748        </a>"#});
2749    }
2750
2751    #[gpui::test]
2752    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2753        let mut cx = NeovimBackedTestContext::new(cx).await;
2754
2755        // f and F
2756        cx.set_shared_state("ˇone two three four").await;
2757        cx.simulate_shared_keystrokes("f o").await;
2758        cx.shared_state().await.assert_eq("one twˇo three four");
2759        cx.simulate_shared_keystrokes(",").await;
2760        cx.shared_state().await.assert_eq("ˇone two three four");
2761        cx.simulate_shared_keystrokes("2 ;").await;
2762        cx.shared_state().await.assert_eq("one two three fˇour");
2763        cx.simulate_shared_keystrokes("shift-f e").await;
2764        cx.shared_state().await.assert_eq("one two threˇe four");
2765        cx.simulate_shared_keystrokes("2 ;").await;
2766        cx.shared_state().await.assert_eq("onˇe two three four");
2767        cx.simulate_shared_keystrokes(",").await;
2768        cx.shared_state().await.assert_eq("one two thrˇee four");
2769
2770        // t and T
2771        cx.set_shared_state("ˇone two three four").await;
2772        cx.simulate_shared_keystrokes("t o").await;
2773        cx.shared_state().await.assert_eq("one tˇwo three four");
2774        cx.simulate_shared_keystrokes(",").await;
2775        cx.shared_state().await.assert_eq("oˇne two three four");
2776        cx.simulate_shared_keystrokes("2 ;").await;
2777        cx.shared_state().await.assert_eq("one two three ˇfour");
2778        cx.simulate_shared_keystrokes("shift-t e").await;
2779        cx.shared_state().await.assert_eq("one two threeˇ four");
2780        cx.simulate_shared_keystrokes("3 ;").await;
2781        cx.shared_state().await.assert_eq("oneˇ two three four");
2782        cx.simulate_shared_keystrokes(",").await;
2783        cx.shared_state().await.assert_eq("one two thˇree four");
2784    }
2785
2786    #[gpui::test]
2787    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2788        let mut cx = NeovimBackedTestContext::new(cx).await;
2789        let initial_state = indoc! {r"something(ˇfoo)"};
2790        cx.set_shared_state(initial_state).await;
2791        cx.simulate_shared_keystrokes("}").await;
2792        cx.shared_state().await.assert_eq("something(fooˇ)");
2793    }
2794
2795    #[gpui::test]
2796    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2797        let mut cx = NeovimBackedTestContext::new(cx).await;
2798        cx.set_shared_state("ˇone\n  two\nthree").await;
2799        cx.simulate_shared_keystrokes("enter").await;
2800        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
2801    }
2802
2803    #[gpui::test]
2804    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2805        let mut cx = NeovimBackedTestContext::new(cx).await;
2806        cx.set_shared_state("ˇ one\n two \nthree").await;
2807        cx.simulate_shared_keystrokes("g _").await;
2808        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2809
2810        cx.set_shared_state("ˇ one \n two \nthree").await;
2811        cx.simulate_shared_keystrokes("g _").await;
2812        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2813        cx.simulate_shared_keystrokes("2 g _").await;
2814        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2815    }
2816
2817    #[gpui::test]
2818    async fn test_window_top(cx: &mut gpui::TestAppContext) {
2819        let mut cx = NeovimBackedTestContext::new(cx).await;
2820        let initial_state = indoc! {r"abc
2821          def
2822          paragraph
2823          the second
2824          third ˇand
2825          final"};
2826
2827        cx.set_shared_state(initial_state).await;
2828        cx.simulate_shared_keystrokes("shift-h").await;
2829        cx.shared_state().await.assert_eq(indoc! {r"abˇc
2830          def
2831          paragraph
2832          the second
2833          third and
2834          final"});
2835
2836        // clip point
2837        cx.set_shared_state(indoc! {r"
2838          1 2 3
2839          4 5 6
2840          7 8 ˇ9
2841          "})
2842            .await;
2843        cx.simulate_shared_keystrokes("shift-h").await;
2844        cx.shared_state().await.assert_eq(indoc! {"
2845          1 2 ˇ3
2846          4 5 6
2847          7 8 9
2848          "});
2849
2850        cx.set_shared_state(indoc! {r"
2851          1 2 3
2852          4 5 6
2853          ˇ7 8 9
2854          "})
2855            .await;
2856        cx.simulate_shared_keystrokes("shift-h").await;
2857        cx.shared_state().await.assert_eq(indoc! {"
2858          ˇ1 2 3
2859          4 5 6
2860          7 8 9
2861          "});
2862
2863        cx.set_shared_state(indoc! {r"
2864          1 2 3
2865          4 5 ˇ6
2866          7 8 9"})
2867            .await;
2868        cx.simulate_shared_keystrokes("9 shift-h").await;
2869        cx.shared_state().await.assert_eq(indoc! {"
2870          1 2 3
2871          4 5 6
2872          7 8 ˇ9"});
2873    }
2874
2875    #[gpui::test]
2876    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2877        let mut cx = NeovimBackedTestContext::new(cx).await;
2878        let initial_state = indoc! {r"abˇc
2879          def
2880          paragraph
2881          the second
2882          third and
2883          final"};
2884
2885        cx.set_shared_state(initial_state).await;
2886        cx.simulate_shared_keystrokes("shift-m").await;
2887        cx.shared_state().await.assert_eq(indoc! {r"abc
2888          def
2889          paˇragraph
2890          the second
2891          third and
2892          final"});
2893
2894        cx.set_shared_state(indoc! {r"
2895          1 2 3
2896          4 5 6
2897          7 8 ˇ9
2898          "})
2899            .await;
2900        cx.simulate_shared_keystrokes("shift-m").await;
2901        cx.shared_state().await.assert_eq(indoc! {"
2902          1 2 3
2903          4 5 ˇ6
2904          7 8 9
2905          "});
2906        cx.set_shared_state(indoc! {r"
2907          1 2 3
2908          4 5 6
2909          ˇ7 8 9
2910          "})
2911            .await;
2912        cx.simulate_shared_keystrokes("shift-m").await;
2913        cx.shared_state().await.assert_eq(indoc! {"
2914          1 2 3
2915          ˇ4 5 6
2916          7 8 9
2917          "});
2918        cx.set_shared_state(indoc! {r"
2919          ˇ1 2 3
2920          4 5 6
2921          7 8 9
2922          "})
2923            .await;
2924        cx.simulate_shared_keystrokes("shift-m").await;
2925        cx.shared_state().await.assert_eq(indoc! {"
2926          1 2 3
2927          ˇ4 5 6
2928          7 8 9
2929          "});
2930        cx.set_shared_state(indoc! {r"
2931          1 2 3
2932          ˇ4 5 6
2933          7 8 9
2934          "})
2935            .await;
2936        cx.simulate_shared_keystrokes("shift-m").await;
2937        cx.shared_state().await.assert_eq(indoc! {"
2938          1 2 3
2939          ˇ4 5 6
2940          7 8 9
2941          "});
2942        cx.set_shared_state(indoc! {r"
2943          1 2 3
2944          4 5 ˇ6
2945          7 8 9
2946          "})
2947            .await;
2948        cx.simulate_shared_keystrokes("shift-m").await;
2949        cx.shared_state().await.assert_eq(indoc! {"
2950          1 2 3
2951          4 5 ˇ6
2952          7 8 9
2953          "});
2954    }
2955
2956    #[gpui::test]
2957    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2958        let mut cx = NeovimBackedTestContext::new(cx).await;
2959        let initial_state = indoc! {r"abc
2960          deˇf
2961          paragraph
2962          the second
2963          third and
2964          final"};
2965
2966        cx.set_shared_state(initial_state).await;
2967        cx.simulate_shared_keystrokes("shift-l").await;
2968        cx.shared_state().await.assert_eq(indoc! {r"abc
2969          def
2970          paragraph
2971          the second
2972          third and
2973          fiˇnal"});
2974
2975        cx.set_shared_state(indoc! {r"
2976          1 2 3
2977          4 5 ˇ6
2978          7 8 9
2979          "})
2980            .await;
2981        cx.simulate_shared_keystrokes("shift-l").await;
2982        cx.shared_state().await.assert_eq(indoc! {"
2983          1 2 3
2984          4 5 6
2985          7 8 9
2986          ˇ"});
2987
2988        cx.set_shared_state(indoc! {r"
2989          1 2 3
2990          ˇ4 5 6
2991          7 8 9
2992          "})
2993            .await;
2994        cx.simulate_shared_keystrokes("shift-l").await;
2995        cx.shared_state().await.assert_eq(indoc! {"
2996          1 2 3
2997          4 5 6
2998          7 8 9
2999          ˇ"});
3000
3001        cx.set_shared_state(indoc! {r"
3002          1 2 ˇ3
3003          4 5 6
3004          7 8 9
3005          "})
3006            .await;
3007        cx.simulate_shared_keystrokes("shift-l").await;
3008        cx.shared_state().await.assert_eq(indoc! {"
3009          1 2 3
3010          4 5 6
3011          7 8 9
3012          ˇ"});
3013
3014        cx.set_shared_state(indoc! {r"
3015          ˇ1 2 3
3016          4 5 6
3017          7 8 9
3018          "})
3019            .await;
3020        cx.simulate_shared_keystrokes("shift-l").await;
3021        cx.shared_state().await.assert_eq(indoc! {"
3022          1 2 3
3023          4 5 6
3024          7 8 9
3025          ˇ"});
3026
3027        cx.set_shared_state(indoc! {r"
3028          1 2 3
3029          4 5 ˇ6
3030          7 8 9
3031          "})
3032            .await;
3033        cx.simulate_shared_keystrokes("9 shift-l").await;
3034        cx.shared_state().await.assert_eq(indoc! {"
3035          1 2 ˇ3
3036          4 5 6
3037          7 8 9
3038          "});
3039    }
3040
3041    #[gpui::test]
3042    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3043        let mut cx = NeovimBackedTestContext::new(cx).await;
3044        cx.set_shared_state(indoc! {r"
3045        456 5ˇ67 678
3046        "})
3047            .await;
3048        cx.simulate_shared_keystrokes("g e").await;
3049        cx.shared_state().await.assert_eq(indoc! {"
3050        45ˇ6 567 678
3051        "});
3052
3053        // Test times
3054        cx.set_shared_state(indoc! {r"
3055        123 234 345
3056        456 5ˇ67 678
3057        "})
3058            .await;
3059        cx.simulate_shared_keystrokes("4 g e").await;
3060        cx.shared_state().await.assert_eq(indoc! {"
3061        12ˇ3 234 345
3062        456 567 678
3063        "});
3064
3065        // With punctuation
3066        cx.set_shared_state(indoc! {r"
3067        123 234 345
3068        4;5.6 5ˇ67 678
3069        789 890 901
3070        "})
3071            .await;
3072        cx.simulate_shared_keystrokes("g e").await;
3073        cx.shared_state().await.assert_eq(indoc! {"
3074          123 234 345
3075          4;5.ˇ6 567 678
3076          789 890 901
3077        "});
3078
3079        // With punctuation and count
3080        cx.set_shared_state(indoc! {r"
3081        123 234 345
3082        4;5.6 5ˇ67 678
3083        789 890 901
3084        "})
3085            .await;
3086        cx.simulate_shared_keystrokes("5 g e").await;
3087        cx.shared_state().await.assert_eq(indoc! {"
3088          123 234 345
3089          ˇ4;5.6 567 678
3090          789 890 901
3091        "});
3092
3093        // newlines
3094        cx.set_shared_state(indoc! {r"
3095        123 234 345
3096
3097        78ˇ9 890 901
3098        "})
3099            .await;
3100        cx.simulate_shared_keystrokes("g e").await;
3101        cx.shared_state().await.assert_eq(indoc! {"
3102          123 234 345
3103          ˇ
3104          789 890 901
3105        "});
3106        cx.simulate_shared_keystrokes("g e").await;
3107        cx.shared_state().await.assert_eq(indoc! {"
3108          123 234 34ˇ5
3109
3110          789 890 901
3111        "});
3112
3113        // With punctuation
3114        cx.set_shared_state(indoc! {r"
3115        123 234 345
3116        4;5.ˇ6 567 678
3117        789 890 901
3118        "})
3119            .await;
3120        cx.simulate_shared_keystrokes("g shift-e").await;
3121        cx.shared_state().await.assert_eq(indoc! {"
3122          123 234 34ˇ5
3123          4;5.6 567 678
3124          789 890 901
3125        "});
3126    }
3127
3128    #[gpui::test]
3129    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3130        let mut cx = NeovimBackedTestContext::new(cx).await;
3131
3132        cx.set_shared_state(indoc! {"
3133            fn aˇ() {
3134              return
3135            }
3136        "})
3137            .await;
3138        cx.simulate_shared_keystrokes("v $ %").await;
3139        cx.shared_state().await.assert_eq(indoc! {"
3140            fn a«() {
3141              return
3142            }ˇ»
3143        "});
3144    }
3145}