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