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