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            | StartOfParagraph
 589            | EndOfParagraph
 590            | WindowTop
 591            | WindowMiddle
 592            | WindowBottom
 593            | NextSectionStart
 594            | NextSectionEnd
 595            | PreviousSectionStart
 596            | PreviousSectionEnd
 597            | NextMethodStart
 598            | NextMethodEnd
 599            | PreviousMethodStart
 600            | PreviousMethodEnd
 601            | NextComment
 602            | PreviousComment
 603            | Jump { line: true, .. } => true,
 604            EndOfLine { .. }
 605            | Matching
 606            | UnmatchedForward { .. }
 607            | UnmatchedBackward { .. }
 608            | FindForward { .. }
 609            | Left
 610            | Backspace
 611            | Right
 612            | SentenceBackward
 613            | SentenceForward
 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 bias = if times < 0 { Bias::Left } else { Bias::Right };
1210    let start = map.display_point_to_fold_point(point, Bias::Left);
1211    let begin_folded_line = map.fold_point_to_display_point(
1212        map.fold_snapshot
1213            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1214    );
1215    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1216
1217    let (goal_wrap, goal_x) = match goal {
1218        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1219        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1220        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1221        _ => {
1222            let x = map.x_for_display_point(point, text_layout_details);
1223            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1224            (select_nth_wrapped_row, x.0)
1225        }
1226    };
1227
1228    let target = start.row() as isize + times;
1229    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1230
1231    let mut begin_folded_line = map.fold_point_to_display_point(
1232        map.fold_snapshot
1233            .clip_point(FoldPoint::new(new_row, 0), bias),
1234    );
1235
1236    let mut i = 0;
1237    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1238        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1239        if map
1240            .display_point_to_fold_point(next_folded_line, bias)
1241            .row()
1242            == new_row
1243        {
1244            i += 1;
1245            begin_folded_line = next_folded_line;
1246        } else {
1247            break;
1248        }
1249    }
1250
1251    let new_col = if i == goal_wrap {
1252        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1253    } else {
1254        map.line_len(begin_folded_line.row())
1255    };
1256
1257    (
1258        map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
1259        goal,
1260    )
1261}
1262
1263fn down_display(
1264    map: &DisplaySnapshot,
1265    mut point: DisplayPoint,
1266    mut goal: SelectionGoal,
1267    times: usize,
1268    text_layout_details: &TextLayoutDetails,
1269) -> (DisplayPoint, SelectionGoal) {
1270    for _ in 0..times {
1271        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1272    }
1273
1274    (point, goal)
1275}
1276
1277fn up_display(
1278    map: &DisplaySnapshot,
1279    mut point: DisplayPoint,
1280    mut goal: SelectionGoal,
1281    times: usize,
1282    text_layout_details: &TextLayoutDetails,
1283) -> (DisplayPoint, SelectionGoal) {
1284    for _ in 0..times {
1285        (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1286    }
1287
1288    (point, goal)
1289}
1290
1291pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1292    for _ in 0..times {
1293        let new_point = movement::saturating_right(map, point);
1294        if point == new_point {
1295            break;
1296        }
1297        point = new_point;
1298    }
1299    point
1300}
1301
1302pub(crate) fn next_char(
1303    map: &DisplaySnapshot,
1304    point: DisplayPoint,
1305    allow_cross_newline: bool,
1306) -> DisplayPoint {
1307    let mut new_point = point;
1308    let mut max_column = map.line_len(new_point.row());
1309    if !allow_cross_newline {
1310        max_column -= 1;
1311    }
1312    if new_point.column() < max_column {
1313        *new_point.column_mut() += 1;
1314    } else if new_point < map.max_point() && allow_cross_newline {
1315        *new_point.row_mut() += 1;
1316        *new_point.column_mut() = 0;
1317    }
1318    map.clip_ignoring_line_ends(new_point, Bias::Right)
1319}
1320
1321pub(crate) fn next_word_start(
1322    map: &DisplaySnapshot,
1323    mut point: DisplayPoint,
1324    ignore_punctuation: bool,
1325    times: usize,
1326) -> DisplayPoint {
1327    let classifier = map
1328        .buffer_snapshot
1329        .char_classifier_at(point.to_point(map))
1330        .ignore_punctuation(ignore_punctuation);
1331    for _ in 0..times {
1332        let mut crossed_newline = false;
1333        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1334            let left_kind = classifier.kind(left);
1335            let right_kind = classifier.kind(right);
1336            let at_newline = right == '\n';
1337
1338            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1339                || at_newline && crossed_newline
1340                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1341
1342            crossed_newline |= at_newline;
1343            found
1344        });
1345        if point == new_point {
1346            break;
1347        }
1348        point = new_point;
1349    }
1350    point
1351}
1352
1353pub(crate) fn next_word_end(
1354    map: &DisplaySnapshot,
1355    mut point: DisplayPoint,
1356    ignore_punctuation: bool,
1357    times: usize,
1358    allow_cross_newline: bool,
1359) -> DisplayPoint {
1360    let classifier = map
1361        .buffer_snapshot
1362        .char_classifier_at(point.to_point(map))
1363        .ignore_punctuation(ignore_punctuation);
1364    for _ in 0..times {
1365        let new_point = next_char(map, point, allow_cross_newline);
1366        let mut need_next_char = false;
1367        let new_point = movement::find_boundary_exclusive(
1368            map,
1369            new_point,
1370            FindRange::MultiLine,
1371            |left, right| {
1372                let left_kind = classifier.kind(left);
1373                let right_kind = classifier.kind(right);
1374                let at_newline = right == '\n';
1375
1376                if !allow_cross_newline && at_newline {
1377                    need_next_char = true;
1378                    return true;
1379                }
1380
1381                left_kind != right_kind && left_kind != CharKind::Whitespace
1382            },
1383        );
1384        let new_point = if need_next_char {
1385            next_char(map, new_point, true)
1386        } else {
1387            new_point
1388        };
1389        let new_point = map.clip_point(new_point, Bias::Left);
1390        if point == new_point {
1391            break;
1392        }
1393        point = new_point;
1394    }
1395    point
1396}
1397
1398fn previous_word_start(
1399    map: &DisplaySnapshot,
1400    mut point: DisplayPoint,
1401    ignore_punctuation: bool,
1402    times: usize,
1403) -> DisplayPoint {
1404    let classifier = map
1405        .buffer_snapshot
1406        .char_classifier_at(point.to_point(map))
1407        .ignore_punctuation(ignore_punctuation);
1408    for _ in 0..times {
1409        // This works even though find_preceding_boundary is called for every character in the line containing
1410        // cursor because the newline is checked only once.
1411        let new_point = movement::find_preceding_boundary_display_point(
1412            map,
1413            point,
1414            FindRange::MultiLine,
1415            |left, right| {
1416                let left_kind = classifier.kind(left);
1417                let right_kind = classifier.kind(right);
1418
1419                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1420            },
1421        );
1422        if point == new_point {
1423            break;
1424        }
1425        point = new_point;
1426    }
1427    point
1428}
1429
1430fn previous_word_end(
1431    map: &DisplaySnapshot,
1432    point: DisplayPoint,
1433    ignore_punctuation: bool,
1434    times: usize,
1435) -> DisplayPoint {
1436    let classifier = map
1437        .buffer_snapshot
1438        .char_classifier_at(point.to_point(map))
1439        .ignore_punctuation(ignore_punctuation);
1440    let mut point = point.to_point(map);
1441
1442    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1443        point.column += 1;
1444    }
1445    for _ in 0..times {
1446        let new_point = movement::find_preceding_boundary_point(
1447            &map.buffer_snapshot,
1448            point,
1449            FindRange::MultiLine,
1450            |left, right| {
1451                let left_kind = classifier.kind(left);
1452                let right_kind = classifier.kind(right);
1453                match (left_kind, right_kind) {
1454                    (CharKind::Punctuation, CharKind::Whitespace)
1455                    | (CharKind::Punctuation, CharKind::Word)
1456                    | (CharKind::Word, CharKind::Whitespace)
1457                    | (CharKind::Word, CharKind::Punctuation) => true,
1458                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1459                    _ => false,
1460                }
1461            },
1462        );
1463        if new_point == point {
1464            break;
1465        }
1466        point = new_point;
1467    }
1468    movement::saturating_left(map, point.to_display_point(map))
1469}
1470
1471fn next_subword_start(
1472    map: &DisplaySnapshot,
1473    mut point: DisplayPoint,
1474    ignore_punctuation: bool,
1475    times: usize,
1476) -> DisplayPoint {
1477    let classifier = map
1478        .buffer_snapshot
1479        .char_classifier_at(point.to_point(map))
1480        .ignore_punctuation(ignore_punctuation);
1481    for _ in 0..times {
1482        let mut crossed_newline = false;
1483        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1484            let left_kind = classifier.kind(left);
1485            let right_kind = classifier.kind(right);
1486            let at_newline = right == '\n';
1487
1488            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1489            let is_subword_start =
1490                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1491
1492            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1493                || at_newline && crossed_newline
1494                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1495
1496            crossed_newline |= at_newline;
1497            found
1498        });
1499        if point == new_point {
1500            break;
1501        }
1502        point = new_point;
1503    }
1504    point
1505}
1506
1507pub(crate) fn next_subword_end(
1508    map: &DisplaySnapshot,
1509    mut point: DisplayPoint,
1510    ignore_punctuation: bool,
1511    times: usize,
1512    allow_cross_newline: bool,
1513) -> DisplayPoint {
1514    let classifier = map
1515        .buffer_snapshot
1516        .char_classifier_at(point.to_point(map))
1517        .ignore_punctuation(ignore_punctuation);
1518    for _ in 0..times {
1519        let new_point = next_char(map, point, allow_cross_newline);
1520
1521        let mut crossed_newline = false;
1522        let mut need_backtrack = false;
1523        let new_point =
1524            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1525                let left_kind = classifier.kind(left);
1526                let right_kind = classifier.kind(right);
1527                let at_newline = right == '\n';
1528
1529                if !allow_cross_newline && at_newline {
1530                    return true;
1531                }
1532
1533                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1534                let is_subword_end =
1535                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1536
1537                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1538
1539                if found && (is_word_end || is_subword_end) {
1540                    need_backtrack = true;
1541                }
1542
1543                crossed_newline |= at_newline;
1544                found
1545            });
1546        let mut new_point = map.clip_point(new_point, Bias::Left);
1547        if need_backtrack {
1548            *new_point.column_mut() -= 1;
1549        }
1550        if point == new_point {
1551            break;
1552        }
1553        point = new_point;
1554    }
1555    point
1556}
1557
1558fn previous_subword_start(
1559    map: &DisplaySnapshot,
1560    mut point: DisplayPoint,
1561    ignore_punctuation: bool,
1562    times: usize,
1563) -> DisplayPoint {
1564    let classifier = map
1565        .buffer_snapshot
1566        .char_classifier_at(point.to_point(map))
1567        .ignore_punctuation(ignore_punctuation);
1568    for _ in 0..times {
1569        let mut crossed_newline = false;
1570        // This works even though find_preceding_boundary is called for every character in the line containing
1571        // cursor because the newline is checked only once.
1572        let new_point = movement::find_preceding_boundary_display_point(
1573            map,
1574            point,
1575            FindRange::MultiLine,
1576            |left, right| {
1577                let left_kind = classifier.kind(left);
1578                let right_kind = classifier.kind(right);
1579                let at_newline = right == '\n';
1580
1581                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1582                let is_subword_start =
1583                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1584
1585                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1586                    || at_newline && crossed_newline
1587                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1588
1589                crossed_newline |= at_newline;
1590
1591                found
1592            },
1593        );
1594        if point == new_point {
1595            break;
1596        }
1597        point = new_point;
1598    }
1599    point
1600}
1601
1602fn previous_subword_end(
1603    map: &DisplaySnapshot,
1604    point: DisplayPoint,
1605    ignore_punctuation: bool,
1606    times: usize,
1607) -> DisplayPoint {
1608    let classifier = map
1609        .buffer_snapshot
1610        .char_classifier_at(point.to_point(map))
1611        .ignore_punctuation(ignore_punctuation);
1612    let mut point = point.to_point(map);
1613
1614    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1615        point.column += 1;
1616    }
1617    for _ in 0..times {
1618        let new_point = movement::find_preceding_boundary_point(
1619            &map.buffer_snapshot,
1620            point,
1621            FindRange::MultiLine,
1622            |left, right| {
1623                let left_kind = classifier.kind(left);
1624                let right_kind = classifier.kind(right);
1625
1626                let is_subword_end =
1627                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1628
1629                if is_subword_end {
1630                    return true;
1631                }
1632
1633                match (left_kind, right_kind) {
1634                    (CharKind::Word, CharKind::Whitespace)
1635                    | (CharKind::Word, CharKind::Punctuation) => true,
1636                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1637                    _ => false,
1638                }
1639            },
1640        );
1641        if new_point == point {
1642            break;
1643        }
1644        point = new_point;
1645    }
1646    movement::saturating_left(map, point.to_display_point(map))
1647}
1648
1649pub(crate) fn first_non_whitespace(
1650    map: &DisplaySnapshot,
1651    display_lines: bool,
1652    from: DisplayPoint,
1653) -> DisplayPoint {
1654    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1655    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1656    for (ch, offset) in map.buffer_chars_at(start_offset) {
1657        if ch == '\n' {
1658            return from;
1659        }
1660
1661        start_offset = offset;
1662
1663        if classifier.kind(ch) != CharKind::Whitespace {
1664            break;
1665        }
1666    }
1667
1668    start_offset.to_display_point(map)
1669}
1670
1671pub(crate) fn last_non_whitespace(
1672    map: &DisplaySnapshot,
1673    from: DisplayPoint,
1674    count: usize,
1675) -> DisplayPoint {
1676    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1677    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1678
1679    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1680    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1681        if classifier.kind(ch) != CharKind::Whitespace {
1682            return end_of_line.to_display_point(map);
1683        }
1684    }
1685
1686    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1687        if ch == '\n' {
1688            break;
1689        }
1690        end_of_line = offset;
1691        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1692            break;
1693        }
1694    }
1695
1696    end_of_line.to_display_point(map)
1697}
1698
1699pub(crate) fn start_of_line(
1700    map: &DisplaySnapshot,
1701    display_lines: bool,
1702    point: DisplayPoint,
1703) -> DisplayPoint {
1704    if display_lines {
1705        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1706    } else {
1707        map.prev_line_boundary(point.to_point(map)).1
1708    }
1709}
1710
1711pub(crate) fn end_of_line(
1712    map: &DisplaySnapshot,
1713    display_lines: bool,
1714    mut point: DisplayPoint,
1715    times: usize,
1716) -> DisplayPoint {
1717    if times > 1 {
1718        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1719    }
1720    if display_lines {
1721        map.clip_point(
1722            DisplayPoint::new(point.row(), map.line_len(point.row())),
1723            Bias::Left,
1724        )
1725    } else {
1726        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1727    }
1728}
1729
1730fn sentence_backwards(
1731    map: &DisplaySnapshot,
1732    point: DisplayPoint,
1733    mut times: usize,
1734) -> DisplayPoint {
1735    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1736    let mut chars = map.reverse_buffer_chars_at(start).peekable();
1737
1738    let mut was_newline = map
1739        .buffer_chars_at(start)
1740        .next()
1741        .is_some_and(|(c, _)| c == '\n');
1742
1743    while let Some((ch, offset)) = chars.next() {
1744        let start_of_next_sentence = if was_newline && ch == '\n' {
1745            Some(offset + ch.len_utf8())
1746        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1747            Some(next_non_blank(map, offset + ch.len_utf8()))
1748        } else if ch == '.' || ch == '?' || ch == '!' {
1749            start_of_next_sentence(map, offset + ch.len_utf8())
1750        } else {
1751            None
1752        };
1753
1754        if let Some(start_of_next_sentence) = start_of_next_sentence {
1755            if start_of_next_sentence < start {
1756                times = times.saturating_sub(1);
1757            }
1758            if times == 0 || offset == 0 {
1759                return map.clip_point(
1760                    start_of_next_sentence
1761                        .to_offset(&map.buffer_snapshot)
1762                        .to_display_point(map),
1763                    Bias::Left,
1764                );
1765            }
1766        }
1767        if was_newline {
1768            start = offset;
1769        }
1770        was_newline = ch == '\n';
1771    }
1772
1773    DisplayPoint::zero()
1774}
1775
1776fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1777    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1778    let mut chars = map.buffer_chars_at(start).peekable();
1779
1780    let mut was_newline = map
1781        .reverse_buffer_chars_at(start)
1782        .next()
1783        .is_some_and(|(c, _)| c == '\n')
1784        && chars.peek().is_some_and(|(c, _)| *c == '\n');
1785
1786    while let Some((ch, offset)) = chars.next() {
1787        if was_newline && ch == '\n' {
1788            continue;
1789        }
1790        let start_of_next_sentence = if was_newline {
1791            Some(next_non_blank(map, offset))
1792        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1793            Some(next_non_blank(map, offset + ch.len_utf8()))
1794        } else if ch == '.' || ch == '?' || ch == '!' {
1795            start_of_next_sentence(map, offset + ch.len_utf8())
1796        } else {
1797            None
1798        };
1799
1800        if let Some(start_of_next_sentence) = start_of_next_sentence {
1801            times = times.saturating_sub(1);
1802            if times == 0 {
1803                return map.clip_point(
1804                    start_of_next_sentence
1805                        .to_offset(&map.buffer_snapshot)
1806                        .to_display_point(map),
1807                    Bias::Right,
1808                );
1809            }
1810        }
1811
1812        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1813    }
1814
1815    map.max_point()
1816}
1817
1818fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1819    for (c, o) in map.buffer_chars_at(start) {
1820        if c == '\n' || !c.is_whitespace() {
1821            return o;
1822        }
1823    }
1824
1825    map.buffer_snapshot.len()
1826}
1827
1828// given the offset after a ., !, or ? find the start of the next sentence.
1829// if this is not a sentence boundary, returns None.
1830fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1831    let chars = map.buffer_chars_at(end_of_sentence);
1832    let mut seen_space = false;
1833
1834    for (char, offset) in chars {
1835        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1836            continue;
1837        }
1838
1839        if char == '\n' && seen_space {
1840            return Some(offset);
1841        } else if char.is_whitespace() {
1842            seen_space = true;
1843        } else if seen_space {
1844            return Some(offset);
1845        } else {
1846            return None;
1847        }
1848    }
1849
1850    Some(map.buffer_snapshot.len())
1851}
1852
1853fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1854    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1855    *new_point.column_mut() = point.column();
1856    map.clip_point(new_point, Bias::Left)
1857}
1858
1859fn end_of_document(
1860    map: &DisplaySnapshot,
1861    point: DisplayPoint,
1862    line: Option<usize>,
1863) -> DisplayPoint {
1864    let new_row = if let Some(line) = line {
1865        (line - 1) as u32
1866    } else {
1867        map.buffer_snapshot.max_row().0
1868    };
1869
1870    let new_point = Point::new(new_row, point.column());
1871    map.clip_point(new_point.to_display_point(map), Bias::Left)
1872}
1873
1874fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
1875    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
1876    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
1877
1878    if head > outer.start && head < inner.start {
1879        let mut offset = inner.end.to_offset(map, Bias::Left);
1880        for c in map.buffer_snapshot.chars_at(offset) {
1881            if c == '/' || c == '\n' || c == '>' {
1882                return Some(offset.to_display_point(map));
1883            }
1884            offset += c.len_utf8();
1885        }
1886    } else {
1887        let mut offset = outer.start.to_offset(map, Bias::Left);
1888        for c in map.buffer_snapshot.chars_at(offset) {
1889            offset += c.len_utf8();
1890            if c == '<' || c == '\n' {
1891                return Some(offset.to_display_point(map));
1892            }
1893        }
1894    }
1895
1896    return None;
1897}
1898
1899fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1900    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1901    let display_point = map.clip_at_line_end(display_point);
1902    let point = display_point.to_point(map);
1903    let offset = point.to_offset(&map.buffer_snapshot);
1904
1905    // Ensure the range is contained by the current line.
1906    let mut line_end = map.next_line_boundary(point).0;
1907    if line_end == point {
1908        line_end = map.max_point().to_point(map);
1909    }
1910
1911    let line_range = map.prev_line_boundary(point).0..line_end;
1912    let visible_line_range =
1913        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1914    let ranges = map
1915        .buffer_snapshot
1916        .bracket_ranges(visible_line_range.clone());
1917    if let Some(ranges) = ranges {
1918        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1919            ..line_range.end.to_offset(&map.buffer_snapshot);
1920        let mut closest_pair_destination = None;
1921        let mut closest_distance = usize::MAX;
1922
1923        for (open_range, close_range) in ranges {
1924            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
1925                if offset > open_range.start && offset < close_range.start {
1926                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1927                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
1928                        return display_point;
1929                    }
1930                    if let Some(tag) = matching_tag(map, display_point) {
1931                        return tag;
1932                    }
1933                } else if close_range.contains(&offset) {
1934                    return open_range.start.to_display_point(map);
1935                } else if open_range.contains(&offset) {
1936                    return (close_range.end - 1).to_display_point(map);
1937                }
1938            }
1939
1940            if open_range.start >= offset && line_range.contains(&open_range.start) {
1941                let distance = open_range.start - offset;
1942                if distance < closest_distance {
1943                    closest_pair_destination = Some(close_range.end - 1);
1944                    closest_distance = distance;
1945                    continue;
1946                }
1947            }
1948
1949            if close_range.start >= offset && line_range.contains(&close_range.start) {
1950                let distance = close_range.start - offset;
1951                if distance < closest_distance {
1952                    closest_pair_destination = Some(open_range.start);
1953                    closest_distance = distance;
1954                    continue;
1955                }
1956            }
1957
1958            continue;
1959        }
1960
1961        closest_pair_destination
1962            .map(|destination| destination.to_display_point(map))
1963            .unwrap_or(display_point)
1964    } else {
1965        display_point
1966    }
1967}
1968
1969fn unmatched_forward(
1970    map: &DisplaySnapshot,
1971    mut display_point: DisplayPoint,
1972    char: char,
1973    times: usize,
1974) -> DisplayPoint {
1975    for _ in 0..times {
1976        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
1977        let point = display_point.to_point(map);
1978        let offset = point.to_offset(&map.buffer_snapshot);
1979
1980        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
1981        let Some(ranges) = ranges else { break };
1982        let mut closest_closing_destination = None;
1983        let mut closest_distance = usize::MAX;
1984
1985        for (_, close_range) in ranges {
1986            if close_range.start > offset {
1987                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1988                if Some(char) == chars.next() {
1989                    let distance = close_range.start - offset;
1990                    if distance < closest_distance {
1991                        closest_closing_destination = Some(close_range.start);
1992                        closest_distance = distance;
1993                        continue;
1994                    }
1995                }
1996            }
1997        }
1998
1999        let new_point = closest_closing_destination
2000            .map(|destination| destination.to_display_point(map))
2001            .unwrap_or(display_point);
2002        if new_point == display_point {
2003            break;
2004        }
2005        display_point = new_point;
2006    }
2007    return display_point;
2008}
2009
2010fn unmatched_backward(
2011    map: &DisplaySnapshot,
2012    mut display_point: DisplayPoint,
2013    char: char,
2014    times: usize,
2015) -> DisplayPoint {
2016    for _ in 0..times {
2017        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2018        let point = display_point.to_point(map);
2019        let offset = point.to_offset(&map.buffer_snapshot);
2020
2021        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2022        let Some(ranges) = ranges else {
2023            break;
2024        };
2025
2026        let mut closest_starting_destination = None;
2027        let mut closest_distance = usize::MAX;
2028
2029        for (start_range, _) in ranges {
2030            if start_range.start < offset {
2031                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2032                if Some(char) == chars.next() {
2033                    let distance = offset - start_range.start;
2034                    if distance < closest_distance {
2035                        closest_starting_destination = Some(start_range.start);
2036                        closest_distance = distance;
2037                        continue;
2038                    }
2039                }
2040            }
2041        }
2042
2043        let new_point = closest_starting_destination
2044            .map(|destination| destination.to_display_point(map))
2045            .unwrap_or(display_point);
2046        if new_point == display_point {
2047            break;
2048        } else {
2049            display_point = new_point;
2050        }
2051    }
2052    display_point
2053}
2054
2055fn find_forward(
2056    map: &DisplaySnapshot,
2057    from: DisplayPoint,
2058    before: bool,
2059    target: char,
2060    times: usize,
2061    mode: FindRange,
2062    smartcase: bool,
2063) -> Option<DisplayPoint> {
2064    let mut to = from;
2065    let mut found = false;
2066
2067    for _ in 0..times {
2068        found = false;
2069        let new_to = find_boundary(map, to, mode, |_, right| {
2070            found = is_character_match(target, right, smartcase);
2071            found
2072        });
2073        if to == new_to {
2074            break;
2075        }
2076        to = new_to;
2077    }
2078
2079    if found {
2080        if before && to.column() > 0 {
2081            *to.column_mut() -= 1;
2082            Some(map.clip_point(to, Bias::Left))
2083        } else {
2084            Some(to)
2085        }
2086    } else {
2087        None
2088    }
2089}
2090
2091fn find_backward(
2092    map: &DisplaySnapshot,
2093    from: DisplayPoint,
2094    after: bool,
2095    target: char,
2096    times: usize,
2097    mode: FindRange,
2098    smartcase: bool,
2099) -> DisplayPoint {
2100    let mut to = from;
2101
2102    for _ in 0..times {
2103        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2104            is_character_match(target, right, smartcase)
2105        });
2106        if to == new_to {
2107            break;
2108        }
2109        to = new_to;
2110    }
2111
2112    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2113    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2114        if after {
2115            *to.column_mut() += 1;
2116            map.clip_point(to, Bias::Right)
2117        } else {
2118            to
2119        }
2120    } else {
2121        from
2122    }
2123}
2124
2125fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2126    if smartcase {
2127        if target.is_uppercase() {
2128            target == other
2129        } else {
2130            target == other.to_ascii_lowercase()
2131        }
2132    } else {
2133        target == other
2134    }
2135}
2136
2137fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2138    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2139    first_non_whitespace(map, false, correct_line)
2140}
2141
2142fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2143    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2144    first_non_whitespace(map, false, correct_line)
2145}
2146
2147fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2148    let correct_line = start_of_relative_buffer_row(map, point, 0);
2149    right(map, correct_line, times.saturating_sub(1))
2150}
2151
2152pub(crate) fn next_line_end(
2153    map: &DisplaySnapshot,
2154    mut point: DisplayPoint,
2155    times: usize,
2156) -> DisplayPoint {
2157    if times > 1 {
2158        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2159    }
2160    end_of_line(map, false, point, 1)
2161}
2162
2163fn window_top(
2164    map: &DisplaySnapshot,
2165    point: DisplayPoint,
2166    text_layout_details: &TextLayoutDetails,
2167    mut times: usize,
2168) -> (DisplayPoint, SelectionGoal) {
2169    let first_visible_line = text_layout_details
2170        .scroll_anchor
2171        .anchor
2172        .to_display_point(map);
2173
2174    if first_visible_line.row() != DisplayRow(0)
2175        && text_layout_details.vertical_scroll_margin as usize > times
2176    {
2177        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2178    }
2179
2180    if let Some(visible_rows) = text_layout_details.visible_rows {
2181        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2182        let new_row = (first_visible_line.row().0 + (times as u32))
2183            .min(bottom_row)
2184            .min(map.max_point().row().0);
2185        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2186
2187        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2188        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2189    } else {
2190        let new_row =
2191            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2192        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2193
2194        let new_point = DisplayPoint::new(new_row, new_col);
2195        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2196    }
2197}
2198
2199fn window_middle(
2200    map: &DisplaySnapshot,
2201    point: DisplayPoint,
2202    text_layout_details: &TextLayoutDetails,
2203) -> (DisplayPoint, SelectionGoal) {
2204    if let Some(visible_rows) = text_layout_details.visible_rows {
2205        let first_visible_line = text_layout_details
2206            .scroll_anchor
2207            .anchor
2208            .to_display_point(map);
2209
2210        let max_visible_rows =
2211            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2212
2213        let new_row =
2214            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2215        let new_row = DisplayRow(new_row);
2216        let new_col = point.column().min(map.line_len(new_row));
2217        let new_point = DisplayPoint::new(new_row, new_col);
2218        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2219    } else {
2220        (point, SelectionGoal::None)
2221    }
2222}
2223
2224fn window_bottom(
2225    map: &DisplaySnapshot,
2226    point: DisplayPoint,
2227    text_layout_details: &TextLayoutDetails,
2228    mut times: usize,
2229) -> (DisplayPoint, SelectionGoal) {
2230    if let Some(visible_rows) = text_layout_details.visible_rows {
2231        let first_visible_line = text_layout_details
2232            .scroll_anchor
2233            .anchor
2234            .to_display_point(map);
2235        let bottom_row = first_visible_line.row().0
2236            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2237        if bottom_row < map.max_point().row().0
2238            && text_layout_details.vertical_scroll_margin as usize > times
2239        {
2240            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2241        }
2242        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2243        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2244        {
2245            first_visible_line.row()
2246        } else {
2247            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2248        };
2249        let new_col = point.column().min(map.line_len(new_row));
2250        let new_point = DisplayPoint::new(new_row, new_col);
2251        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2252    } else {
2253        (point, SelectionGoal::None)
2254    }
2255}
2256
2257fn method_motion(
2258    map: &DisplaySnapshot,
2259    mut display_point: DisplayPoint,
2260    times: usize,
2261    direction: Direction,
2262    is_start: bool,
2263) -> DisplayPoint {
2264    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2265        return display_point;
2266    };
2267
2268    for _ in 0..times {
2269        let point = map.display_point_to_point(display_point, Bias::Left);
2270        let offset = point.to_offset(&map.buffer_snapshot);
2271        let range = if direction == Direction::Prev {
2272            0..offset
2273        } else {
2274            offset..buffer.len()
2275        };
2276
2277        let possibilities = buffer
2278            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2279            .filter_map(|(range, object)| {
2280                if !matches!(object, language::TextObject::AroundFunction) {
2281                    return None;
2282                }
2283
2284                let relevant = if is_start { range.start } else { range.end };
2285                if direction == Direction::Prev && relevant < offset {
2286                    Some(relevant)
2287                } else if direction == Direction::Next && relevant > offset + 1 {
2288                    Some(relevant)
2289                } else {
2290                    None
2291                }
2292            });
2293
2294        let dest = if direction == Direction::Prev {
2295            possibilities.max().unwrap_or(offset)
2296        } else {
2297            possibilities.min().unwrap_or(offset)
2298        };
2299        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2300        if new_point == display_point {
2301            break;
2302        }
2303        display_point = new_point;
2304    }
2305    display_point
2306}
2307
2308fn comment_motion(
2309    map: &DisplaySnapshot,
2310    mut display_point: DisplayPoint,
2311    times: usize,
2312    direction: Direction,
2313) -> DisplayPoint {
2314    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2315        return display_point;
2316    };
2317
2318    for _ in 0..times {
2319        let point = map.display_point_to_point(display_point, Bias::Left);
2320        let offset = point.to_offset(&map.buffer_snapshot);
2321        let range = if direction == Direction::Prev {
2322            0..offset
2323        } else {
2324            offset..buffer.len()
2325        };
2326
2327        let possibilities = buffer
2328            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2329            .filter_map(|(range, object)| {
2330                if !matches!(object, language::TextObject::AroundComment) {
2331                    return None;
2332                }
2333
2334                let relevant = if direction == Direction::Prev {
2335                    range.start
2336                } else {
2337                    range.end
2338                };
2339                if direction == Direction::Prev && relevant < offset {
2340                    Some(relevant)
2341                } else if direction == Direction::Next && relevant > offset + 1 {
2342                    Some(relevant)
2343                } else {
2344                    None
2345                }
2346            });
2347
2348        let dest = if direction == Direction::Prev {
2349            possibilities.max().unwrap_or(offset)
2350        } else {
2351            possibilities.min().unwrap_or(offset)
2352        };
2353        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2354        if new_point == display_point {
2355            break;
2356        }
2357        display_point = new_point;
2358    }
2359
2360    display_point
2361}
2362
2363fn section_motion(
2364    map: &DisplaySnapshot,
2365    mut display_point: DisplayPoint,
2366    times: usize,
2367    direction: Direction,
2368    is_start: bool,
2369) -> DisplayPoint {
2370    if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
2371        for _ in 0..times {
2372            let offset = map
2373                .display_point_to_point(display_point, Bias::Left)
2374                .to_offset(&map.buffer_snapshot);
2375            let range = if direction == Direction::Prev {
2376                0..offset
2377            } else {
2378                offset..buffer.len()
2379            };
2380
2381            // we set a max start depth here because we want a section to only be "top level"
2382            // similar to vim's default of '{' in the first column.
2383            // (and without it, ]] at the start of editor.rs is -very- slow)
2384            let mut possibilities = buffer
2385                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2386                .filter(|(_, object)| {
2387                    matches!(
2388                        object,
2389                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2390                    )
2391                })
2392                .collect::<Vec<_>>();
2393            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2394            let mut prev_end = None;
2395            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2396                if t == language::TextObject::AroundFunction
2397                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2398                {
2399                    return None;
2400                }
2401                prev_end = Some(range.end);
2402
2403                let relevant = if is_start { range.start } else { range.end };
2404                if direction == Direction::Prev && relevant < offset {
2405                    Some(relevant)
2406                } else if direction == Direction::Next && relevant > offset + 1 {
2407                    Some(relevant)
2408                } else {
2409                    None
2410                }
2411            });
2412
2413            let offset = if direction == Direction::Prev {
2414                possibilities.max().unwrap_or(0)
2415            } else {
2416                possibilities.min().unwrap_or(buffer.len())
2417            };
2418
2419            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2420            if new_point == display_point {
2421                break;
2422            }
2423            display_point = new_point;
2424        }
2425        return display_point;
2426    };
2427
2428    for _ in 0..times {
2429        let point = map.display_point_to_point(display_point, Bias::Left);
2430        let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2431            return display_point;
2432        };
2433        let next_point = match (direction, is_start) {
2434            (Direction::Prev, true) => {
2435                let mut start = excerpt.start_anchor().to_display_point(&map);
2436                if start >= display_point && start.row() > DisplayRow(0) {
2437                    let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2438                        return display_point;
2439                    };
2440                    start = excerpt.start_anchor().to_display_point(&map);
2441                }
2442                start
2443            }
2444            (Direction::Prev, false) => {
2445                let mut start = excerpt.start_anchor().to_display_point(&map);
2446                if start.row() > DisplayRow(0) {
2447                    *start.row_mut() -= 1;
2448                }
2449                map.clip_point(start, Bias::Left)
2450            }
2451            (Direction::Next, true) => {
2452                let mut end = excerpt.end_anchor().to_display_point(&map);
2453                *end.row_mut() += 1;
2454                map.clip_point(end, Bias::Right)
2455            }
2456            (Direction::Next, false) => {
2457                let mut end = excerpt.end_anchor().to_display_point(&map);
2458                *end.column_mut() = 0;
2459                if end <= display_point {
2460                    *end.row_mut() += 1;
2461                    let point_end = map.display_point_to_point(end, Bias::Right);
2462                    let Some(excerpt) =
2463                        map.buffer_snapshot.excerpt_containing(point_end..point_end)
2464                    else {
2465                        return display_point;
2466                    };
2467                    end = excerpt.end_anchor().to_display_point(&map);
2468                    *end.column_mut() = 0;
2469                }
2470                end
2471            }
2472        };
2473        if next_point == display_point {
2474            break;
2475        }
2476        display_point = next_point;
2477    }
2478
2479    display_point
2480}
2481
2482#[cfg(test)]
2483mod test {
2484
2485    use crate::{
2486        state::Mode,
2487        test::{NeovimBackedTestContext, VimTestContext},
2488    };
2489    use editor::display_map::Inlay;
2490    use indoc::indoc;
2491
2492    #[gpui::test]
2493    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2494        let mut cx = NeovimBackedTestContext::new(cx).await;
2495
2496        let initial_state = indoc! {r"ˇabc
2497            def
2498
2499            paragraph
2500            the second
2501
2502
2503
2504            third and
2505            final"};
2506
2507        // goes down once
2508        cx.set_shared_state(initial_state).await;
2509        cx.simulate_shared_keystrokes("}").await;
2510        cx.shared_state().await.assert_eq(indoc! {r"abc
2511            def
2512            ˇ
2513            paragraph
2514            the second
2515
2516
2517
2518            third and
2519            final"});
2520
2521        // goes up once
2522        cx.simulate_shared_keystrokes("{").await;
2523        cx.shared_state().await.assert_eq(initial_state);
2524
2525        // goes down twice
2526        cx.simulate_shared_keystrokes("2 }").await;
2527        cx.shared_state().await.assert_eq(indoc! {r"abc
2528            def
2529
2530            paragraph
2531            the second
2532            ˇ
2533
2534
2535            third and
2536            final"});
2537
2538        // goes down over multiple blanks
2539        cx.simulate_shared_keystrokes("}").await;
2540        cx.shared_state().await.assert_eq(indoc! {r"abc
2541                def
2542
2543                paragraph
2544                the second
2545
2546
2547
2548                third and
2549                finaˇl"});
2550
2551        // goes up twice
2552        cx.simulate_shared_keystrokes("2 {").await;
2553        cx.shared_state().await.assert_eq(indoc! {r"abc
2554                def
2555                ˇ
2556                paragraph
2557                the second
2558
2559
2560
2561                third and
2562                final"});
2563    }
2564
2565    #[gpui::test]
2566    async fn test_matching(cx: &mut gpui::TestAppContext) {
2567        let mut cx = NeovimBackedTestContext::new(cx).await;
2568
2569        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2570                do(something(with<Types>.and_arrays[0, 2]))
2571            }"})
2572            .await;
2573        cx.simulate_shared_keystrokes("%").await;
2574        cx.shared_state()
2575            .await
2576            .assert_eq(indoc! {r"func (a stringˇ) {
2577                do(something(with<Types>.and_arrays[0, 2]))
2578            }"});
2579
2580        // test it works on the last character of the line
2581        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2582            do(something(with<Types>.and_arrays[0, 2]))
2583            }"})
2584            .await;
2585        cx.simulate_shared_keystrokes("%").await;
2586        cx.shared_state()
2587            .await
2588            .assert_eq(indoc! {r"func (a string) {
2589            do(something(with<Types>.and_arrays[0, 2]))
2590            ˇ}"});
2591
2592        // test it works on immediate nesting
2593        cx.set_shared_state("ˇ{()}").await;
2594        cx.simulate_shared_keystrokes("%").await;
2595        cx.shared_state().await.assert_eq("{()ˇ}");
2596        cx.simulate_shared_keystrokes("%").await;
2597        cx.shared_state().await.assert_eq("ˇ{()}");
2598
2599        // test it works on immediate nesting inside braces
2600        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2601        cx.simulate_shared_keystrokes("%").await;
2602        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2603
2604        // test it jumps to the next paren on a line
2605        cx.set_shared_state("func ˇboop() {\n}").await;
2606        cx.simulate_shared_keystrokes("%").await;
2607        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2608    }
2609
2610    #[gpui::test]
2611    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2612        let mut cx = NeovimBackedTestContext::new(cx).await;
2613
2614        // test it works with curly braces
2615        cx.set_shared_state(indoc! {r"func (a string) {
2616                do(something(with<Types>.anˇd_arrays[0, 2]))
2617            }"})
2618            .await;
2619        cx.simulate_shared_keystrokes("] }").await;
2620        cx.shared_state()
2621            .await
2622            .assert_eq(indoc! {r"func (a string) {
2623                do(something(with<Types>.and_arrays[0, 2]))
2624            ˇ}"});
2625
2626        // test it works with brackets
2627        cx.set_shared_state(indoc! {r"func (a string) {
2628                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2629            }"})
2630            .await;
2631        cx.simulate_shared_keystrokes("] )").await;
2632        cx.shared_state()
2633            .await
2634            .assert_eq(indoc! {r"func (a string) {
2635                do(something(with<Types>.and_arrays[0, 2])ˇ)
2636            }"});
2637
2638        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2639            .await;
2640        cx.simulate_shared_keystrokes("] )").await;
2641        cx.shared_state()
2642            .await
2643            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2644
2645        // test it works on immediate nesting
2646        cx.set_shared_state("{ˇ {}{}}").await;
2647        cx.simulate_shared_keystrokes("] }").await;
2648        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2649        cx.set_shared_state("(ˇ ()())").await;
2650        cx.simulate_shared_keystrokes("] )").await;
2651        cx.shared_state().await.assert_eq("( ()()ˇ)");
2652
2653        // test it works on immediate nesting inside braces
2654        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2655        cx.simulate_shared_keystrokes("] }").await;
2656        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2657        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2658        cx.simulate_shared_keystrokes("] )").await;
2659        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2660    }
2661
2662    #[gpui::test]
2663    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2664        let mut cx = NeovimBackedTestContext::new(cx).await;
2665
2666        // test it works with curly braces
2667        cx.set_shared_state(indoc! {r"func (a string) {
2668                do(something(with<Types>.anˇd_arrays[0, 2]))
2669            }"})
2670            .await;
2671        cx.simulate_shared_keystrokes("[ {").await;
2672        cx.shared_state()
2673            .await
2674            .assert_eq(indoc! {r"func (a string) ˇ{
2675                do(something(with<Types>.and_arrays[0, 2]))
2676            }"});
2677
2678        // test it works with brackets
2679        cx.set_shared_state(indoc! {r"func (a string) {
2680                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2681            }"})
2682            .await;
2683        cx.simulate_shared_keystrokes("[ (").await;
2684        cx.shared_state()
2685            .await
2686            .assert_eq(indoc! {r"func (a string) {
2687                doˇ(something(with<Types>.and_arrays[0, 2]))
2688            }"});
2689
2690        // test it works on immediate nesting
2691        cx.set_shared_state("{{}{} ˇ }").await;
2692        cx.simulate_shared_keystrokes("[ {").await;
2693        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2694        cx.set_shared_state("(()() ˇ )").await;
2695        cx.simulate_shared_keystrokes("[ (").await;
2696        cx.shared_state().await.assert_eq("ˇ(()()  )");
2697
2698        // test it works on immediate nesting inside braces
2699        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2700        cx.simulate_shared_keystrokes("[ {").await;
2701        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2702        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2703        cx.simulate_shared_keystrokes("[ (").await;
2704        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2705    }
2706
2707    #[gpui::test]
2708    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2709        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2710
2711        cx.neovim.exec("set filetype=html").await;
2712
2713        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2714        cx.simulate_shared_keystrokes("%").await;
2715        cx.shared_state()
2716            .await
2717            .assert_eq(indoc! {r"<body><ˇ/body>"});
2718        cx.simulate_shared_keystrokes("%").await;
2719
2720        // test jumping backwards
2721        cx.shared_state()
2722            .await
2723            .assert_eq(indoc! {r"<ˇbody></body>"});
2724
2725        // test self-closing tags
2726        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2727        cx.simulate_shared_keystrokes("%").await;
2728        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2729
2730        // test tag with attributes
2731        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2732            </div>
2733            "})
2734            .await;
2735        cx.simulate_shared_keystrokes("%").await;
2736        cx.shared_state()
2737            .await
2738            .assert_eq(indoc! {r"<div class='test' id='main'>
2739            <ˇ/div>
2740            "});
2741
2742        // test multi-line self-closing tag
2743        cx.set_shared_state(indoc! {r#"<a>
2744            <br
2745                test = "test"
2746            /ˇ>
2747        </a>"#})
2748            .await;
2749        cx.simulate_shared_keystrokes("%").await;
2750        cx.shared_state().await.assert_eq(indoc! {r#"<a>
2751            ˇ<br
2752                test = "test"
2753            />
2754        </a>"#});
2755    }
2756
2757    #[gpui::test]
2758    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2759        let mut cx = NeovimBackedTestContext::new(cx).await;
2760
2761        // f and F
2762        cx.set_shared_state("ˇone two three four").await;
2763        cx.simulate_shared_keystrokes("f o").await;
2764        cx.shared_state().await.assert_eq("one twˇo three four");
2765        cx.simulate_shared_keystrokes(",").await;
2766        cx.shared_state().await.assert_eq("ˇone two three four");
2767        cx.simulate_shared_keystrokes("2 ;").await;
2768        cx.shared_state().await.assert_eq("one two three fˇour");
2769        cx.simulate_shared_keystrokes("shift-f e").await;
2770        cx.shared_state().await.assert_eq("one two threˇe four");
2771        cx.simulate_shared_keystrokes("2 ;").await;
2772        cx.shared_state().await.assert_eq("onˇe two three four");
2773        cx.simulate_shared_keystrokes(",").await;
2774        cx.shared_state().await.assert_eq("one two thrˇee four");
2775
2776        // t and T
2777        cx.set_shared_state("ˇone two three four").await;
2778        cx.simulate_shared_keystrokes("t o").await;
2779        cx.shared_state().await.assert_eq("one tˇwo three four");
2780        cx.simulate_shared_keystrokes(",").await;
2781        cx.shared_state().await.assert_eq("oˇne two three four");
2782        cx.simulate_shared_keystrokes("2 ;").await;
2783        cx.shared_state().await.assert_eq("one two three ˇfour");
2784        cx.simulate_shared_keystrokes("shift-t e").await;
2785        cx.shared_state().await.assert_eq("one two threeˇ four");
2786        cx.simulate_shared_keystrokes("3 ;").await;
2787        cx.shared_state().await.assert_eq("oneˇ two three four");
2788        cx.simulate_shared_keystrokes(",").await;
2789        cx.shared_state().await.assert_eq("one two thˇree four");
2790    }
2791
2792    #[gpui::test]
2793    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2794        let mut cx = NeovimBackedTestContext::new(cx).await;
2795        let initial_state = indoc! {r"something(ˇfoo)"};
2796        cx.set_shared_state(initial_state).await;
2797        cx.simulate_shared_keystrokes("}").await;
2798        cx.shared_state().await.assert_eq("something(fooˇ)");
2799    }
2800
2801    #[gpui::test]
2802    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2803        let mut cx = NeovimBackedTestContext::new(cx).await;
2804        cx.set_shared_state("ˇone\n  two\nthree").await;
2805        cx.simulate_shared_keystrokes("enter").await;
2806        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
2807    }
2808
2809    #[gpui::test]
2810    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2811        let mut cx = NeovimBackedTestContext::new(cx).await;
2812        cx.set_shared_state("ˇ one\n two \nthree").await;
2813        cx.simulate_shared_keystrokes("g _").await;
2814        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2815
2816        cx.set_shared_state("ˇ one \n two \nthree").await;
2817        cx.simulate_shared_keystrokes("g _").await;
2818        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2819        cx.simulate_shared_keystrokes("2 g _").await;
2820        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2821    }
2822
2823    #[gpui::test]
2824    async fn test_window_top(cx: &mut gpui::TestAppContext) {
2825        let mut cx = NeovimBackedTestContext::new(cx).await;
2826        let initial_state = indoc! {r"abc
2827          def
2828          paragraph
2829          the second
2830          third ˇand
2831          final"};
2832
2833        cx.set_shared_state(initial_state).await;
2834        cx.simulate_shared_keystrokes("shift-h").await;
2835        cx.shared_state().await.assert_eq(indoc! {r"abˇc
2836          def
2837          paragraph
2838          the second
2839          third and
2840          final"});
2841
2842        // clip point
2843        cx.set_shared_state(indoc! {r"
2844          1 2 3
2845          4 5 6
2846          7 8 ˇ9
2847          "})
2848            .await;
2849        cx.simulate_shared_keystrokes("shift-h").await;
2850        cx.shared_state().await.assert_eq(indoc! {"
2851          1 2 ˇ3
2852          4 5 6
2853          7 8 9
2854          "});
2855
2856        cx.set_shared_state(indoc! {r"
2857          1 2 3
2858          4 5 6
2859          ˇ7 8 9
2860          "})
2861            .await;
2862        cx.simulate_shared_keystrokes("shift-h").await;
2863        cx.shared_state().await.assert_eq(indoc! {"
2864          ˇ1 2 3
2865          4 5 6
2866          7 8 9
2867          "});
2868
2869        cx.set_shared_state(indoc! {r"
2870          1 2 3
2871          4 5 ˇ6
2872          7 8 9"})
2873            .await;
2874        cx.simulate_shared_keystrokes("9 shift-h").await;
2875        cx.shared_state().await.assert_eq(indoc! {"
2876          1 2 3
2877          4 5 6
2878          7 8 ˇ9"});
2879    }
2880
2881    #[gpui::test]
2882    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2883        let mut cx = NeovimBackedTestContext::new(cx).await;
2884        let initial_state = indoc! {r"abˇc
2885          def
2886          paragraph
2887          the second
2888          third and
2889          final"};
2890
2891        cx.set_shared_state(initial_state).await;
2892        cx.simulate_shared_keystrokes("shift-m").await;
2893        cx.shared_state().await.assert_eq(indoc! {r"abc
2894          def
2895          paˇragraph
2896          the second
2897          third and
2898          final"});
2899
2900        cx.set_shared_state(indoc! {r"
2901          1 2 3
2902          4 5 6
2903          7 8 ˇ9
2904          "})
2905            .await;
2906        cx.simulate_shared_keystrokes("shift-m").await;
2907        cx.shared_state().await.assert_eq(indoc! {"
2908          1 2 3
2909          4 5 ˇ6
2910          7 8 9
2911          "});
2912        cx.set_shared_state(indoc! {r"
2913          1 2 3
2914          4 5 6
2915          ˇ7 8 9
2916          "})
2917            .await;
2918        cx.simulate_shared_keystrokes("shift-m").await;
2919        cx.shared_state().await.assert_eq(indoc! {"
2920          1 2 3
2921          ˇ4 5 6
2922          7 8 9
2923          "});
2924        cx.set_shared_state(indoc! {r"
2925          ˇ1 2 3
2926          4 5 6
2927          7 8 9
2928          "})
2929            .await;
2930        cx.simulate_shared_keystrokes("shift-m").await;
2931        cx.shared_state().await.assert_eq(indoc! {"
2932          1 2 3
2933          ˇ4 5 6
2934          7 8 9
2935          "});
2936        cx.set_shared_state(indoc! {r"
2937          1 2 3
2938          ˇ4 5 6
2939          7 8 9
2940          "})
2941            .await;
2942        cx.simulate_shared_keystrokes("shift-m").await;
2943        cx.shared_state().await.assert_eq(indoc! {"
2944          1 2 3
2945          ˇ4 5 6
2946          7 8 9
2947          "});
2948        cx.set_shared_state(indoc! {r"
2949          1 2 3
2950          4 5 ˇ6
2951          7 8 9
2952          "})
2953            .await;
2954        cx.simulate_shared_keystrokes("shift-m").await;
2955        cx.shared_state().await.assert_eq(indoc! {"
2956          1 2 3
2957          4 5 ˇ6
2958          7 8 9
2959          "});
2960    }
2961
2962    #[gpui::test]
2963    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2964        let mut cx = NeovimBackedTestContext::new(cx).await;
2965        let initial_state = indoc! {r"abc
2966          deˇf
2967          paragraph
2968          the second
2969          third and
2970          final"};
2971
2972        cx.set_shared_state(initial_state).await;
2973        cx.simulate_shared_keystrokes("shift-l").await;
2974        cx.shared_state().await.assert_eq(indoc! {r"abc
2975          def
2976          paragraph
2977          the second
2978          third and
2979          fiˇnal"});
2980
2981        cx.set_shared_state(indoc! {r"
2982          1 2 3
2983          4 5 ˇ6
2984          7 8 9
2985          "})
2986            .await;
2987        cx.simulate_shared_keystrokes("shift-l").await;
2988        cx.shared_state().await.assert_eq(indoc! {"
2989          1 2 3
2990          4 5 6
2991          7 8 9
2992          ˇ"});
2993
2994        cx.set_shared_state(indoc! {r"
2995          1 2 3
2996          ˇ4 5 6
2997          7 8 9
2998          "})
2999            .await;
3000        cx.simulate_shared_keystrokes("shift-l").await;
3001        cx.shared_state().await.assert_eq(indoc! {"
3002          1 2 3
3003          4 5 6
3004          7 8 9
3005          ˇ"});
3006
3007        cx.set_shared_state(indoc! {r"
3008          1 2 ˇ3
3009          4 5 6
3010          7 8 9
3011          "})
3012            .await;
3013        cx.simulate_shared_keystrokes("shift-l").await;
3014        cx.shared_state().await.assert_eq(indoc! {"
3015          1 2 3
3016          4 5 6
3017          7 8 9
3018          ˇ"});
3019
3020        cx.set_shared_state(indoc! {r"
3021          ˇ1 2 3
3022          4 5 6
3023          7 8 9
3024          "})
3025            .await;
3026        cx.simulate_shared_keystrokes("shift-l").await;
3027        cx.shared_state().await.assert_eq(indoc! {"
3028          1 2 3
3029          4 5 6
3030          7 8 9
3031          ˇ"});
3032
3033        cx.set_shared_state(indoc! {r"
3034          1 2 3
3035          4 5 ˇ6
3036          7 8 9
3037          "})
3038            .await;
3039        cx.simulate_shared_keystrokes("9 shift-l").await;
3040        cx.shared_state().await.assert_eq(indoc! {"
3041          1 2 ˇ3
3042          4 5 6
3043          7 8 9
3044          "});
3045    }
3046
3047    #[gpui::test]
3048    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3049        let mut cx = NeovimBackedTestContext::new(cx).await;
3050        cx.set_shared_state(indoc! {r"
3051        456 5ˇ67 678
3052        "})
3053            .await;
3054        cx.simulate_shared_keystrokes("g e").await;
3055        cx.shared_state().await.assert_eq(indoc! {"
3056        45ˇ6 567 678
3057        "});
3058
3059        // Test times
3060        cx.set_shared_state(indoc! {r"
3061        123 234 345
3062        456 5ˇ67 678
3063        "})
3064            .await;
3065        cx.simulate_shared_keystrokes("4 g e").await;
3066        cx.shared_state().await.assert_eq(indoc! {"
3067        12ˇ3 234 345
3068        456 567 678
3069        "});
3070
3071        // With punctuation
3072        cx.set_shared_state(indoc! {r"
3073        123 234 345
3074        4;5.6 5ˇ67 678
3075        789 890 901
3076        "})
3077            .await;
3078        cx.simulate_shared_keystrokes("g e").await;
3079        cx.shared_state().await.assert_eq(indoc! {"
3080          123 234 345
3081          4;5.ˇ6 567 678
3082          789 890 901
3083        "});
3084
3085        // With punctuation and count
3086        cx.set_shared_state(indoc! {r"
3087        123 234 345
3088        4;5.6 5ˇ67 678
3089        789 890 901
3090        "})
3091            .await;
3092        cx.simulate_shared_keystrokes("5 g e").await;
3093        cx.shared_state().await.assert_eq(indoc! {"
3094          123 234 345
3095          ˇ4;5.6 567 678
3096          789 890 901
3097        "});
3098
3099        // newlines
3100        cx.set_shared_state(indoc! {r"
3101        123 234 345
3102
3103        78ˇ9 890 901
3104        "})
3105            .await;
3106        cx.simulate_shared_keystrokes("g e").await;
3107        cx.shared_state().await.assert_eq(indoc! {"
3108          123 234 345
3109          ˇ
3110          789 890 901
3111        "});
3112        cx.simulate_shared_keystrokes("g e").await;
3113        cx.shared_state().await.assert_eq(indoc! {"
3114          123 234 34ˇ5
3115
3116          789 890 901
3117        "});
3118
3119        // With punctuation
3120        cx.set_shared_state(indoc! {r"
3121        123 234 345
3122        4;5.ˇ6 567 678
3123        789 890 901
3124        "})
3125            .await;
3126        cx.simulate_shared_keystrokes("g shift-e").await;
3127        cx.shared_state().await.assert_eq(indoc! {"
3128          123 234 34ˇ5
3129          4;5.6 567 678
3130          789 890 901
3131        "});
3132    }
3133
3134    #[gpui::test]
3135    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3136        let mut cx = NeovimBackedTestContext::new(cx).await;
3137
3138        cx.set_shared_state(indoc! {"
3139            fn aˇ() {
3140              return
3141            }
3142        "})
3143            .await;
3144        cx.simulate_shared_keystrokes("v $ %").await;
3145        cx.shared_state().await.assert_eq(indoc! {"
3146            fn a«() {
3147              return
3148            }ˇ»
3149        "});
3150    }
3151
3152    #[gpui::test]
3153    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3154        let mut cx = VimTestContext::new(cx, true).await;
3155
3156        cx.set_state(
3157            indoc! {"
3158            struct Foo {
3159            ˇ
3160            }
3161        "},
3162            Mode::Normal,
3163        );
3164
3165        cx.update_editor(|editor, cx| {
3166            let range = editor.selections.newest_anchor().range();
3167            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3168            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3169            editor.splice_inlays(vec![], vec![inlay], cx);
3170        });
3171
3172        cx.simulate_keystrokes("j");
3173        cx.assert_state(
3174            indoc! {"
3175            struct Foo {
3176
3177            ˇ}
3178        "},
3179            Mode::Normal,
3180        );
3181    }
3182}