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