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        if point == new_point {
1657            break;
1658        }
1659        point = new_point;
1660    }
1661    point
1662}
1663
1664fn previous_subword_start(
1665    map: &DisplaySnapshot,
1666    mut point: DisplayPoint,
1667    ignore_punctuation: bool,
1668    times: usize,
1669) -> DisplayPoint {
1670    let classifier = map
1671        .buffer_snapshot
1672        .char_classifier_at(point.to_point(map))
1673        .ignore_punctuation(ignore_punctuation);
1674    for _ in 0..times {
1675        let mut crossed_newline = false;
1676        // This works even though find_preceding_boundary is called for every character in the line containing
1677        // cursor because the newline is checked only once.
1678        let new_point = movement::find_preceding_boundary_display_point(
1679            map,
1680            point,
1681            FindRange::MultiLine,
1682            |left, right| {
1683                let left_kind = classifier.kind(left);
1684                let right_kind = classifier.kind(right);
1685                let at_newline = right == '\n';
1686
1687                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1688                let is_subword_start =
1689                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1690
1691                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1692                    || at_newline && crossed_newline
1693                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1694
1695                crossed_newline |= at_newline;
1696
1697                found
1698            },
1699        );
1700        if point == new_point {
1701            break;
1702        }
1703        point = new_point;
1704    }
1705    point
1706}
1707
1708fn previous_subword_end(
1709    map: &DisplaySnapshot,
1710    point: DisplayPoint,
1711    ignore_punctuation: bool,
1712    times: usize,
1713) -> DisplayPoint {
1714    let classifier = map
1715        .buffer_snapshot
1716        .char_classifier_at(point.to_point(map))
1717        .ignore_punctuation(ignore_punctuation);
1718    let mut point = point.to_point(map);
1719
1720    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1721        point.column += 1;
1722    }
1723    for _ in 0..times {
1724        let new_point = movement::find_preceding_boundary_point(
1725            &map.buffer_snapshot,
1726            point,
1727            FindRange::MultiLine,
1728            |left, right| {
1729                let left_kind = classifier.kind(left);
1730                let right_kind = classifier.kind(right);
1731
1732                let is_subword_end =
1733                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1734
1735                if is_subword_end {
1736                    return true;
1737                }
1738
1739                match (left_kind, right_kind) {
1740                    (CharKind::Word, CharKind::Whitespace)
1741                    | (CharKind::Word, CharKind::Punctuation) => true,
1742                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1743                    _ => false,
1744                }
1745            },
1746        );
1747        if new_point == point {
1748            break;
1749        }
1750        point = new_point;
1751    }
1752    movement::saturating_left(map, point.to_display_point(map))
1753}
1754
1755pub(crate) fn first_non_whitespace(
1756    map: &DisplaySnapshot,
1757    display_lines: bool,
1758    from: DisplayPoint,
1759) -> DisplayPoint {
1760    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1761    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1762    for (ch, offset) in map.buffer_chars_at(start_offset) {
1763        if ch == '\n' {
1764            return from;
1765        }
1766
1767        start_offset = offset;
1768
1769        if classifier.kind(ch) != CharKind::Whitespace {
1770            break;
1771        }
1772    }
1773
1774    start_offset.to_display_point(map)
1775}
1776
1777pub(crate) fn last_non_whitespace(
1778    map: &DisplaySnapshot,
1779    from: DisplayPoint,
1780    count: usize,
1781) -> DisplayPoint {
1782    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1783    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1784
1785    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1786    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1787        if classifier.kind(ch) != CharKind::Whitespace {
1788            return end_of_line.to_display_point(map);
1789        }
1790    }
1791
1792    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1793        if ch == '\n' {
1794            break;
1795        }
1796        end_of_line = offset;
1797        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1798            break;
1799        }
1800    }
1801
1802    end_of_line.to_display_point(map)
1803}
1804
1805pub(crate) fn start_of_line(
1806    map: &DisplaySnapshot,
1807    display_lines: bool,
1808    point: DisplayPoint,
1809) -> DisplayPoint {
1810    if display_lines {
1811        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1812    } else {
1813        map.prev_line_boundary(point.to_point(map)).1
1814    }
1815}
1816
1817pub(crate) fn end_of_line(
1818    map: &DisplaySnapshot,
1819    display_lines: bool,
1820    mut point: DisplayPoint,
1821    times: usize,
1822) -> DisplayPoint {
1823    if times > 1 {
1824        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1825    }
1826    if display_lines {
1827        map.clip_point(
1828            DisplayPoint::new(point.row(), map.line_len(point.row())),
1829            Bias::Left,
1830        )
1831    } else {
1832        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1833    }
1834}
1835
1836fn sentence_backwards(
1837    map: &DisplaySnapshot,
1838    point: DisplayPoint,
1839    mut times: usize,
1840) -> DisplayPoint {
1841    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1842    let mut chars = map.reverse_buffer_chars_at(start).peekable();
1843
1844    let mut was_newline = map
1845        .buffer_chars_at(start)
1846        .next()
1847        .is_some_and(|(c, _)| c == '\n');
1848
1849    while let Some((ch, offset)) = chars.next() {
1850        let start_of_next_sentence = if was_newline && ch == '\n' {
1851            Some(offset + ch.len_utf8())
1852        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1853            Some(next_non_blank(map, offset + ch.len_utf8()))
1854        } else if ch == '.' || ch == '?' || ch == '!' {
1855            start_of_next_sentence(map, offset + ch.len_utf8())
1856        } else {
1857            None
1858        };
1859
1860        if let Some(start_of_next_sentence) = start_of_next_sentence {
1861            if start_of_next_sentence < start {
1862                times = times.saturating_sub(1);
1863            }
1864            if times == 0 || offset == 0 {
1865                return map.clip_point(
1866                    start_of_next_sentence
1867                        .to_offset(&map.buffer_snapshot)
1868                        .to_display_point(map),
1869                    Bias::Left,
1870                );
1871            }
1872        }
1873        if was_newline {
1874            start = offset;
1875        }
1876        was_newline = ch == '\n';
1877    }
1878
1879    DisplayPoint::zero()
1880}
1881
1882fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1883    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1884    let mut chars = map.buffer_chars_at(start).peekable();
1885
1886    let mut was_newline = map
1887        .reverse_buffer_chars_at(start)
1888        .next()
1889        .is_some_and(|(c, _)| c == '\n')
1890        && chars.peek().is_some_and(|(c, _)| *c == '\n');
1891
1892    while let Some((ch, offset)) = chars.next() {
1893        if was_newline && ch == '\n' {
1894            continue;
1895        }
1896        let start_of_next_sentence = if was_newline {
1897            Some(next_non_blank(map, offset))
1898        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1899            Some(next_non_blank(map, offset + ch.len_utf8()))
1900        } else if ch == '.' || ch == '?' || ch == '!' {
1901            start_of_next_sentence(map, offset + ch.len_utf8())
1902        } else {
1903            None
1904        };
1905
1906        if let Some(start_of_next_sentence) = start_of_next_sentence {
1907            times = times.saturating_sub(1);
1908            if times == 0 {
1909                return map.clip_point(
1910                    start_of_next_sentence
1911                        .to_offset(&map.buffer_snapshot)
1912                        .to_display_point(map),
1913                    Bias::Right,
1914                );
1915            }
1916        }
1917
1918        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1919    }
1920
1921    map.max_point()
1922}
1923
1924fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1925    for (c, o) in map.buffer_chars_at(start) {
1926        if c == '\n' || !c.is_whitespace() {
1927            return o;
1928        }
1929    }
1930
1931    map.buffer_snapshot.len()
1932}
1933
1934// given the offset after a ., !, or ? find the start of the next sentence.
1935// if this is not a sentence boundary, returns None.
1936fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1937    let chars = map.buffer_chars_at(end_of_sentence);
1938    let mut seen_space = false;
1939
1940    for (char, offset) in chars {
1941        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1942            continue;
1943        }
1944
1945        if char == '\n' && seen_space {
1946            return Some(offset);
1947        } else if char.is_whitespace() {
1948            seen_space = true;
1949        } else if seen_space {
1950            return Some(offset);
1951        } else {
1952            return None;
1953        }
1954    }
1955
1956    Some(map.buffer_snapshot.len())
1957}
1958
1959fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1960    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1961    *new_point.column_mut() = point.column();
1962    map.clip_point(new_point, Bias::Left)
1963}
1964
1965fn end_of_document(
1966    map: &DisplaySnapshot,
1967    point: DisplayPoint,
1968    line: Option<usize>,
1969) -> DisplayPoint {
1970    let new_row = if let Some(line) = line {
1971        (line - 1) as u32
1972    } else {
1973        map.buffer_snapshot.max_row().0
1974    };
1975
1976    let new_point = Point::new(new_row, point.column());
1977    map.clip_point(new_point.to_display_point(map), Bias::Left)
1978}
1979
1980fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
1981    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
1982    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
1983
1984    if head > outer.start && head < inner.start {
1985        let mut offset = inner.end.to_offset(map, Bias::Left);
1986        for c in map.buffer_snapshot.chars_at(offset) {
1987            if c == '/' || c == '\n' || c == '>' {
1988                return Some(offset.to_display_point(map));
1989            }
1990            offset += c.len_utf8();
1991        }
1992    } else {
1993        let mut offset = outer.start.to_offset(map, Bias::Left);
1994        for c in map.buffer_snapshot.chars_at(offset) {
1995            offset += c.len_utf8();
1996            if c == '<' || c == '\n' {
1997                return Some(offset.to_display_point(map));
1998            }
1999        }
2000    }
2001
2002    return None;
2003}
2004
2005fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2006    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2007    let display_point = map.clip_at_line_end(display_point);
2008    let point = display_point.to_point(map);
2009    let offset = point.to_offset(&map.buffer_snapshot);
2010
2011    // Ensure the range is contained by the current line.
2012    let mut line_end = map.next_line_boundary(point).0;
2013    if line_end == point {
2014        line_end = map.max_point().to_point(map);
2015    }
2016
2017    let line_range = map.prev_line_boundary(point).0..line_end;
2018    let visible_line_range =
2019        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2020    let ranges = map
2021        .buffer_snapshot
2022        .bracket_ranges(visible_line_range.clone());
2023    if let Some(ranges) = ranges {
2024        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2025            ..line_range.end.to_offset(&map.buffer_snapshot);
2026        let mut closest_pair_destination = None;
2027        let mut closest_distance = usize::MAX;
2028
2029        for (open_range, close_range) in ranges {
2030            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2031                if offset > open_range.start && offset < close_range.start {
2032                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2033                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2034                        return display_point;
2035                    }
2036                    if let Some(tag) = matching_tag(map, display_point) {
2037                        return tag;
2038                    }
2039                } else if close_range.contains(&offset) {
2040                    return open_range.start.to_display_point(map);
2041                } else if open_range.contains(&offset) {
2042                    return (close_range.end - 1).to_display_point(map);
2043                }
2044            }
2045
2046            if (open_range.contains(&offset) || open_range.start >= offset)
2047                && line_range.contains(&open_range.start)
2048            {
2049                let distance = open_range.start.saturating_sub(offset);
2050                if distance < closest_distance {
2051                    closest_pair_destination = Some(close_range.start);
2052                    closest_distance = distance;
2053                    continue;
2054                }
2055            }
2056
2057            if (close_range.contains(&offset) || close_range.start >= offset)
2058                && line_range.contains(&close_range.start)
2059            {
2060                let distance = close_range.start.saturating_sub(offset);
2061                if distance < closest_distance {
2062                    closest_pair_destination = Some(open_range.start);
2063                    closest_distance = distance;
2064                    continue;
2065                }
2066            }
2067
2068            continue;
2069        }
2070
2071        closest_pair_destination
2072            .map(|destination| destination.to_display_point(map))
2073            .unwrap_or(display_point)
2074    } else {
2075        display_point
2076    }
2077}
2078
2079fn unmatched_forward(
2080    map: &DisplaySnapshot,
2081    mut display_point: DisplayPoint,
2082    char: char,
2083    times: usize,
2084) -> DisplayPoint {
2085    for _ in 0..times {
2086        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2087        let point = display_point.to_point(map);
2088        let offset = point.to_offset(&map.buffer_snapshot);
2089
2090        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2091        let Some(ranges) = ranges else { break };
2092        let mut closest_closing_destination = None;
2093        let mut closest_distance = usize::MAX;
2094
2095        for (_, close_range) in ranges {
2096            if close_range.start > offset {
2097                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2098                if Some(char) == chars.next() {
2099                    let distance = close_range.start - offset;
2100                    if distance < closest_distance {
2101                        closest_closing_destination = Some(close_range.start);
2102                        closest_distance = distance;
2103                        continue;
2104                    }
2105                }
2106            }
2107        }
2108
2109        let new_point = closest_closing_destination
2110            .map(|destination| destination.to_display_point(map))
2111            .unwrap_or(display_point);
2112        if new_point == display_point {
2113            break;
2114        }
2115        display_point = new_point;
2116    }
2117    return display_point;
2118}
2119
2120fn unmatched_backward(
2121    map: &DisplaySnapshot,
2122    mut display_point: DisplayPoint,
2123    char: char,
2124    times: usize,
2125) -> DisplayPoint {
2126    for _ in 0..times {
2127        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2128        let point = display_point.to_point(map);
2129        let offset = point.to_offset(&map.buffer_snapshot);
2130
2131        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2132        let Some(ranges) = ranges else {
2133            break;
2134        };
2135
2136        let mut closest_starting_destination = None;
2137        let mut closest_distance = usize::MAX;
2138
2139        for (start_range, _) in ranges {
2140            if start_range.start < offset {
2141                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2142                if Some(char) == chars.next() {
2143                    let distance = offset - start_range.start;
2144                    if distance < closest_distance {
2145                        closest_starting_destination = Some(start_range.start);
2146                        closest_distance = distance;
2147                        continue;
2148                    }
2149                }
2150            }
2151        }
2152
2153        let new_point = closest_starting_destination
2154            .map(|destination| destination.to_display_point(map))
2155            .unwrap_or(display_point);
2156        if new_point == display_point {
2157            break;
2158        } else {
2159            display_point = new_point;
2160        }
2161    }
2162    display_point
2163}
2164
2165fn find_forward(
2166    map: &DisplaySnapshot,
2167    from: DisplayPoint,
2168    before: bool,
2169    target: char,
2170    times: usize,
2171    mode: FindRange,
2172    smartcase: bool,
2173) -> Option<DisplayPoint> {
2174    let mut to = from;
2175    let mut found = false;
2176
2177    for _ in 0..times {
2178        found = false;
2179        let new_to = find_boundary(map, to, mode, |_, right| {
2180            found = is_character_match(target, right, smartcase);
2181            found
2182        });
2183        if to == new_to {
2184            break;
2185        }
2186        to = new_to;
2187    }
2188
2189    if found {
2190        if before && to.column() > 0 {
2191            *to.column_mut() -= 1;
2192            Some(map.clip_point(to, Bias::Left))
2193        } else {
2194            Some(to)
2195        }
2196    } else {
2197        None
2198    }
2199}
2200
2201fn find_backward(
2202    map: &DisplaySnapshot,
2203    from: DisplayPoint,
2204    after: bool,
2205    target: char,
2206    times: usize,
2207    mode: FindRange,
2208    smartcase: bool,
2209) -> DisplayPoint {
2210    let mut to = from;
2211
2212    for _ in 0..times {
2213        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2214            is_character_match(target, right, smartcase)
2215        });
2216        if to == new_to {
2217            break;
2218        }
2219        to = new_to;
2220    }
2221
2222    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2223    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2224        if after {
2225            *to.column_mut() += 1;
2226            map.clip_point(to, Bias::Right)
2227        } else {
2228            to
2229        }
2230    } else {
2231        from
2232    }
2233}
2234
2235fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2236    if smartcase {
2237        if target.is_uppercase() {
2238            target == other
2239        } else {
2240            target == other.to_ascii_lowercase()
2241        }
2242    } else {
2243        target == other
2244    }
2245}
2246
2247fn sneak(
2248    map: &DisplaySnapshot,
2249    from: DisplayPoint,
2250    first_target: char,
2251    second_target: char,
2252    times: usize,
2253    smartcase: bool,
2254) -> Option<DisplayPoint> {
2255    let mut to = from;
2256    let mut found = false;
2257
2258    for _ in 0..times {
2259        found = false;
2260        let new_to = find_boundary(
2261            map,
2262            movement::right(map, to),
2263            FindRange::MultiLine,
2264            |left, right| {
2265                found = is_character_match(first_target, left, smartcase)
2266                    && is_character_match(second_target, right, smartcase);
2267                found
2268            },
2269        );
2270        if to == new_to {
2271            break;
2272        }
2273        to = new_to;
2274    }
2275
2276    if found {
2277        Some(movement::left(map, to))
2278    } else {
2279        None
2280    }
2281}
2282
2283fn sneak_backward(
2284    map: &DisplaySnapshot,
2285    from: DisplayPoint,
2286    first_target: char,
2287    second_target: char,
2288    times: usize,
2289    smartcase: bool,
2290) -> Option<DisplayPoint> {
2291    let mut to = from;
2292    let mut found = false;
2293
2294    for _ in 0..times {
2295        found = false;
2296        let new_to =
2297            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2298                found = is_character_match(first_target, left, smartcase)
2299                    && is_character_match(second_target, right, smartcase);
2300                found
2301            });
2302        if to == new_to {
2303            break;
2304        }
2305        to = new_to;
2306    }
2307
2308    if found {
2309        Some(movement::left(map, to))
2310    } else {
2311        None
2312    }
2313}
2314
2315fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2316    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2317    first_non_whitespace(map, false, correct_line)
2318}
2319
2320fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2321    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2322    first_non_whitespace(map, false, correct_line)
2323}
2324
2325fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2326    let correct_line = start_of_relative_buffer_row(map, point, 0);
2327    right(map, correct_line, times.saturating_sub(1))
2328}
2329
2330pub(crate) fn next_line_end(
2331    map: &DisplaySnapshot,
2332    mut point: DisplayPoint,
2333    times: usize,
2334) -> DisplayPoint {
2335    if times > 1 {
2336        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2337    }
2338    end_of_line(map, false, point, 1)
2339}
2340
2341fn window_top(
2342    map: &DisplaySnapshot,
2343    point: DisplayPoint,
2344    text_layout_details: &TextLayoutDetails,
2345    mut times: usize,
2346) -> (DisplayPoint, SelectionGoal) {
2347    let first_visible_line = text_layout_details
2348        .scroll_anchor
2349        .anchor
2350        .to_display_point(map);
2351
2352    if first_visible_line.row() != DisplayRow(0)
2353        && text_layout_details.vertical_scroll_margin as usize > times
2354    {
2355        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2356    }
2357
2358    if let Some(visible_rows) = text_layout_details.visible_rows {
2359        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2360        let new_row = (first_visible_line.row().0 + (times as u32))
2361            .min(bottom_row)
2362            .min(map.max_point().row().0);
2363        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2364
2365        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2366        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2367    } else {
2368        let new_row =
2369            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2370        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2371
2372        let new_point = DisplayPoint::new(new_row, new_col);
2373        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2374    }
2375}
2376
2377fn window_middle(
2378    map: &DisplaySnapshot,
2379    point: DisplayPoint,
2380    text_layout_details: &TextLayoutDetails,
2381) -> (DisplayPoint, SelectionGoal) {
2382    if let Some(visible_rows) = text_layout_details.visible_rows {
2383        let first_visible_line = text_layout_details
2384            .scroll_anchor
2385            .anchor
2386            .to_display_point(map);
2387
2388        let max_visible_rows =
2389            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2390
2391        let new_row =
2392            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2393        let new_row = DisplayRow(new_row);
2394        let new_col = point.column().min(map.line_len(new_row));
2395        let new_point = DisplayPoint::new(new_row, new_col);
2396        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2397    } else {
2398        (point, SelectionGoal::None)
2399    }
2400}
2401
2402fn window_bottom(
2403    map: &DisplaySnapshot,
2404    point: DisplayPoint,
2405    text_layout_details: &TextLayoutDetails,
2406    mut times: usize,
2407) -> (DisplayPoint, SelectionGoal) {
2408    if let Some(visible_rows) = text_layout_details.visible_rows {
2409        let first_visible_line = text_layout_details
2410            .scroll_anchor
2411            .anchor
2412            .to_display_point(map);
2413        let bottom_row = first_visible_line.row().0
2414            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2415        if bottom_row < map.max_point().row().0
2416            && text_layout_details.vertical_scroll_margin as usize > times
2417        {
2418            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2419        }
2420        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2421        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2422        {
2423            first_visible_line.row()
2424        } else {
2425            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2426        };
2427        let new_col = point.column().min(map.line_len(new_row));
2428        let new_point = DisplayPoint::new(new_row, new_col);
2429        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2430    } else {
2431        (point, SelectionGoal::None)
2432    }
2433}
2434
2435fn method_motion(
2436    map: &DisplaySnapshot,
2437    mut display_point: DisplayPoint,
2438    times: usize,
2439    direction: Direction,
2440    is_start: bool,
2441) -> DisplayPoint {
2442    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2443        return display_point;
2444    };
2445
2446    for _ in 0..times {
2447        let point = map.display_point_to_point(display_point, Bias::Left);
2448        let offset = point.to_offset(&map.buffer_snapshot);
2449        let range = if direction == Direction::Prev {
2450            0..offset
2451        } else {
2452            offset..buffer.len()
2453        };
2454
2455        let possibilities = buffer
2456            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2457            .filter_map(|(range, object)| {
2458                if !matches!(object, language::TextObject::AroundFunction) {
2459                    return None;
2460                }
2461
2462                let relevant = if is_start { range.start } else { range.end };
2463                if direction == Direction::Prev && relevant < offset {
2464                    Some(relevant)
2465                } else if direction == Direction::Next && relevant > offset + 1 {
2466                    Some(relevant)
2467                } else {
2468                    None
2469                }
2470            });
2471
2472        let dest = if direction == Direction::Prev {
2473            possibilities.max().unwrap_or(offset)
2474        } else {
2475            possibilities.min().unwrap_or(offset)
2476        };
2477        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2478        if new_point == display_point {
2479            break;
2480        }
2481        display_point = new_point;
2482    }
2483    display_point
2484}
2485
2486fn comment_motion(
2487    map: &DisplaySnapshot,
2488    mut display_point: DisplayPoint,
2489    times: usize,
2490    direction: Direction,
2491) -> DisplayPoint {
2492    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2493        return display_point;
2494    };
2495
2496    for _ in 0..times {
2497        let point = map.display_point_to_point(display_point, Bias::Left);
2498        let offset = point.to_offset(&map.buffer_snapshot);
2499        let range = if direction == Direction::Prev {
2500            0..offset
2501        } else {
2502            offset..buffer.len()
2503        };
2504
2505        let possibilities = buffer
2506            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2507            .filter_map(|(range, object)| {
2508                if !matches!(object, language::TextObject::AroundComment) {
2509                    return None;
2510                }
2511
2512                let relevant = if direction == Direction::Prev {
2513                    range.start
2514                } else {
2515                    range.end
2516                };
2517                if direction == Direction::Prev && relevant < offset {
2518                    Some(relevant)
2519                } else if direction == Direction::Next && relevant > offset + 1 {
2520                    Some(relevant)
2521                } else {
2522                    None
2523                }
2524            });
2525
2526        let dest = if direction == Direction::Prev {
2527            possibilities.max().unwrap_or(offset)
2528        } else {
2529            possibilities.min().unwrap_or(offset)
2530        };
2531        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2532        if new_point == display_point {
2533            break;
2534        }
2535        display_point = new_point;
2536    }
2537
2538    display_point
2539}
2540
2541fn section_motion(
2542    map: &DisplaySnapshot,
2543    mut display_point: DisplayPoint,
2544    times: usize,
2545    direction: Direction,
2546    is_start: bool,
2547) -> DisplayPoint {
2548    if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
2549        for _ in 0..times {
2550            let offset = map
2551                .display_point_to_point(display_point, Bias::Left)
2552                .to_offset(&map.buffer_snapshot);
2553            let range = if direction == Direction::Prev {
2554                0..offset
2555            } else {
2556                offset..buffer.len()
2557            };
2558
2559            // we set a max start depth here because we want a section to only be "top level"
2560            // similar to vim's default of '{' in the first column.
2561            // (and without it, ]] at the start of editor.rs is -very- slow)
2562            let mut possibilities = buffer
2563                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2564                .filter(|(_, object)| {
2565                    matches!(
2566                        object,
2567                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2568                    )
2569                })
2570                .collect::<Vec<_>>();
2571            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2572            let mut prev_end = None;
2573            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2574                if t == language::TextObject::AroundFunction
2575                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2576                {
2577                    return None;
2578                }
2579                prev_end = Some(range.end);
2580
2581                let relevant = if is_start { range.start } else { range.end };
2582                if direction == Direction::Prev && relevant < offset {
2583                    Some(relevant)
2584                } else if direction == Direction::Next && relevant > offset + 1 {
2585                    Some(relevant)
2586                } else {
2587                    None
2588                }
2589            });
2590
2591            let offset = if direction == Direction::Prev {
2592                possibilities.max().unwrap_or(0)
2593            } else {
2594                possibilities.min().unwrap_or(buffer.len())
2595            };
2596
2597            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2598            if new_point == display_point {
2599                break;
2600            }
2601            display_point = new_point;
2602        }
2603        return display_point;
2604    };
2605
2606    for _ in 0..times {
2607        let point = map.display_point_to_point(display_point, Bias::Left);
2608        let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2609            return display_point;
2610        };
2611        let next_point = match (direction, is_start) {
2612            (Direction::Prev, true) => {
2613                let mut start = excerpt.start_anchor().to_display_point(&map);
2614                if start >= display_point && start.row() > DisplayRow(0) {
2615                    let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2616                        return display_point;
2617                    };
2618                    start = excerpt.start_anchor().to_display_point(&map);
2619                }
2620                start
2621            }
2622            (Direction::Prev, false) => {
2623                let mut start = excerpt.start_anchor().to_display_point(&map);
2624                if start.row() > DisplayRow(0) {
2625                    *start.row_mut() -= 1;
2626                }
2627                map.clip_point(start, Bias::Left)
2628            }
2629            (Direction::Next, true) => {
2630                let mut end = excerpt.end_anchor().to_display_point(&map);
2631                *end.row_mut() += 1;
2632                map.clip_point(end, Bias::Right)
2633            }
2634            (Direction::Next, false) => {
2635                let mut end = excerpt.end_anchor().to_display_point(&map);
2636                *end.column_mut() = 0;
2637                if end <= display_point {
2638                    *end.row_mut() += 1;
2639                    let point_end = map.display_point_to_point(end, Bias::Right);
2640                    let Some(excerpt) =
2641                        map.buffer_snapshot.excerpt_containing(point_end..point_end)
2642                    else {
2643                        return display_point;
2644                    };
2645                    end = excerpt.end_anchor().to_display_point(&map);
2646                    *end.column_mut() = 0;
2647                }
2648                end
2649            }
2650        };
2651        if next_point == display_point {
2652            break;
2653        }
2654        display_point = next_point;
2655    }
2656
2657    display_point
2658}
2659
2660#[cfg(test)]
2661mod test {
2662
2663    use crate::{
2664        state::Mode,
2665        test::{NeovimBackedTestContext, VimTestContext},
2666    };
2667    use editor::display_map::Inlay;
2668    use indoc::indoc;
2669
2670    #[gpui::test]
2671    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2672        let mut cx = NeovimBackedTestContext::new(cx).await;
2673
2674        let initial_state = indoc! {r"ˇabc
2675            def
2676
2677            paragraph
2678            the second
2679
2680
2681
2682            third and
2683            final"};
2684
2685        // goes down once
2686        cx.set_shared_state(initial_state).await;
2687        cx.simulate_shared_keystrokes("}").await;
2688        cx.shared_state().await.assert_eq(indoc! {r"abc
2689            def
2690            ˇ
2691            paragraph
2692            the second
2693
2694
2695
2696            third and
2697            final"});
2698
2699        // goes up once
2700        cx.simulate_shared_keystrokes("{").await;
2701        cx.shared_state().await.assert_eq(initial_state);
2702
2703        // goes down twice
2704        cx.simulate_shared_keystrokes("2 }").await;
2705        cx.shared_state().await.assert_eq(indoc! {r"abc
2706            def
2707
2708            paragraph
2709            the second
2710            ˇ
2711
2712
2713            third and
2714            final"});
2715
2716        // goes down over multiple blanks
2717        cx.simulate_shared_keystrokes("}").await;
2718        cx.shared_state().await.assert_eq(indoc! {r"abc
2719                def
2720
2721                paragraph
2722                the second
2723
2724
2725
2726                third and
2727                finaˇl"});
2728
2729        // goes up twice
2730        cx.simulate_shared_keystrokes("2 {").await;
2731        cx.shared_state().await.assert_eq(indoc! {r"abc
2732                def
2733                ˇ
2734                paragraph
2735                the second
2736
2737
2738
2739                third and
2740                final"});
2741    }
2742
2743    #[gpui::test]
2744    async fn test_matching(cx: &mut gpui::TestAppContext) {
2745        let mut cx = NeovimBackedTestContext::new(cx).await;
2746
2747        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2748                do(something(with<Types>.and_arrays[0, 2]))
2749            }"})
2750            .await;
2751        cx.simulate_shared_keystrokes("%").await;
2752        cx.shared_state()
2753            .await
2754            .assert_eq(indoc! {r"func (a stringˇ) {
2755                do(something(with<Types>.and_arrays[0, 2]))
2756            }"});
2757
2758        // test it works on the last character of the line
2759        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2760            do(something(with<Types>.and_arrays[0, 2]))
2761            }"})
2762            .await;
2763        cx.simulate_shared_keystrokes("%").await;
2764        cx.shared_state()
2765            .await
2766            .assert_eq(indoc! {r"func (a string) {
2767            do(something(with<Types>.and_arrays[0, 2]))
2768            ˇ}"});
2769
2770        // test it works on immediate nesting
2771        cx.set_shared_state("ˇ{()}").await;
2772        cx.simulate_shared_keystrokes("%").await;
2773        cx.shared_state().await.assert_eq("{()ˇ}");
2774        cx.simulate_shared_keystrokes("%").await;
2775        cx.shared_state().await.assert_eq("ˇ{()}");
2776
2777        // test it works on immediate nesting inside braces
2778        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2779        cx.simulate_shared_keystrokes("%").await;
2780        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2781
2782        // test it jumps to the next paren on a line
2783        cx.set_shared_state("func ˇboop() {\n}").await;
2784        cx.simulate_shared_keystrokes("%").await;
2785        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2786    }
2787
2788    #[gpui::test]
2789    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2790        let mut cx = NeovimBackedTestContext::new(cx).await;
2791
2792        // test it works with curly braces
2793        cx.set_shared_state(indoc! {r"func (a string) {
2794                do(something(with<Types>.anˇd_arrays[0, 2]))
2795            }"})
2796            .await;
2797        cx.simulate_shared_keystrokes("] }").await;
2798        cx.shared_state()
2799            .await
2800            .assert_eq(indoc! {r"func (a string) {
2801                do(something(with<Types>.and_arrays[0, 2]))
2802            ˇ}"});
2803
2804        // test it works with brackets
2805        cx.set_shared_state(indoc! {r"func (a string) {
2806                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2807            }"})
2808            .await;
2809        cx.simulate_shared_keystrokes("] )").await;
2810        cx.shared_state()
2811            .await
2812            .assert_eq(indoc! {r"func (a string) {
2813                do(something(with<Types>.and_arrays[0, 2])ˇ)
2814            }"});
2815
2816        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2817            .await;
2818        cx.simulate_shared_keystrokes("] )").await;
2819        cx.shared_state()
2820            .await
2821            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2822
2823        // test it works on immediate nesting
2824        cx.set_shared_state("{ˇ {}{}}").await;
2825        cx.simulate_shared_keystrokes("] }").await;
2826        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2827        cx.set_shared_state("(ˇ ()())").await;
2828        cx.simulate_shared_keystrokes("] )").await;
2829        cx.shared_state().await.assert_eq("( ()()ˇ)");
2830
2831        // test it works on immediate nesting inside braces
2832        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2833        cx.simulate_shared_keystrokes("] }").await;
2834        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2835        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2836        cx.simulate_shared_keystrokes("] )").await;
2837        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2838    }
2839
2840    #[gpui::test]
2841    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2842        let mut cx = NeovimBackedTestContext::new(cx).await;
2843
2844        // test it works with curly braces
2845        cx.set_shared_state(indoc! {r"func (a string) {
2846                do(something(with<Types>.anˇd_arrays[0, 2]))
2847            }"})
2848            .await;
2849        cx.simulate_shared_keystrokes("[ {").await;
2850        cx.shared_state()
2851            .await
2852            .assert_eq(indoc! {r"func (a string) ˇ{
2853                do(something(with<Types>.and_arrays[0, 2]))
2854            }"});
2855
2856        // test it works with brackets
2857        cx.set_shared_state(indoc! {r"func (a string) {
2858                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2859            }"})
2860            .await;
2861        cx.simulate_shared_keystrokes("[ (").await;
2862        cx.shared_state()
2863            .await
2864            .assert_eq(indoc! {r"func (a string) {
2865                doˇ(something(with<Types>.and_arrays[0, 2]))
2866            }"});
2867
2868        // test it works on immediate nesting
2869        cx.set_shared_state("{{}{} ˇ }").await;
2870        cx.simulate_shared_keystrokes("[ {").await;
2871        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2872        cx.set_shared_state("(()() ˇ )").await;
2873        cx.simulate_shared_keystrokes("[ (").await;
2874        cx.shared_state().await.assert_eq("ˇ(()()  )");
2875
2876        // test it works on immediate nesting inside braces
2877        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2878        cx.simulate_shared_keystrokes("[ {").await;
2879        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2880        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2881        cx.simulate_shared_keystrokes("[ (").await;
2882        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2883    }
2884
2885    #[gpui::test]
2886    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2887        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2888
2889        cx.neovim.exec("set filetype=html").await;
2890
2891        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2892        cx.simulate_shared_keystrokes("%").await;
2893        cx.shared_state()
2894            .await
2895            .assert_eq(indoc! {r"<body><ˇ/body>"});
2896        cx.simulate_shared_keystrokes("%").await;
2897
2898        // test jumping backwards
2899        cx.shared_state()
2900            .await
2901            .assert_eq(indoc! {r"<ˇbody></body>"});
2902
2903        // test self-closing tags
2904        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2905        cx.simulate_shared_keystrokes("%").await;
2906        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2907
2908        // test tag with attributes
2909        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2910            </div>
2911            "})
2912            .await;
2913        cx.simulate_shared_keystrokes("%").await;
2914        cx.shared_state()
2915            .await
2916            .assert_eq(indoc! {r"<div class='test' id='main'>
2917            <ˇ/div>
2918            "});
2919
2920        // test multi-line self-closing tag
2921        cx.set_shared_state(indoc! {r#"<a>
2922            <br
2923                test = "test"
2924            /ˇ>
2925        </a>"#})
2926            .await;
2927        cx.simulate_shared_keystrokes("%").await;
2928        cx.shared_state().await.assert_eq(indoc! {r#"<a>
2929            ˇ<br
2930                test = "test"
2931            />
2932        </a>"#});
2933    }
2934
2935    #[gpui::test]
2936    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2937        let mut cx = NeovimBackedTestContext::new(cx).await;
2938
2939        // f and F
2940        cx.set_shared_state("ˇone two three four").await;
2941        cx.simulate_shared_keystrokes("f o").await;
2942        cx.shared_state().await.assert_eq("one twˇo three four");
2943        cx.simulate_shared_keystrokes(",").await;
2944        cx.shared_state().await.assert_eq("ˇone two three four");
2945        cx.simulate_shared_keystrokes("2 ;").await;
2946        cx.shared_state().await.assert_eq("one two three fˇour");
2947        cx.simulate_shared_keystrokes("shift-f e").await;
2948        cx.shared_state().await.assert_eq("one two threˇe four");
2949        cx.simulate_shared_keystrokes("2 ;").await;
2950        cx.shared_state().await.assert_eq("onˇe two three four");
2951        cx.simulate_shared_keystrokes(",").await;
2952        cx.shared_state().await.assert_eq("one two thrˇee four");
2953
2954        // t and T
2955        cx.set_shared_state("ˇone two three four").await;
2956        cx.simulate_shared_keystrokes("t o").await;
2957        cx.shared_state().await.assert_eq("one tˇwo three four");
2958        cx.simulate_shared_keystrokes(",").await;
2959        cx.shared_state().await.assert_eq("oˇne two three four");
2960        cx.simulate_shared_keystrokes("2 ;").await;
2961        cx.shared_state().await.assert_eq("one two three ˇfour");
2962        cx.simulate_shared_keystrokes("shift-t e").await;
2963        cx.shared_state().await.assert_eq("one two threeˇ four");
2964        cx.simulate_shared_keystrokes("3 ;").await;
2965        cx.shared_state().await.assert_eq("oneˇ two three four");
2966        cx.simulate_shared_keystrokes(",").await;
2967        cx.shared_state().await.assert_eq("one two thˇree four");
2968    }
2969
2970    #[gpui::test]
2971    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2972        let mut cx = NeovimBackedTestContext::new(cx).await;
2973        let initial_state = indoc! {r"something(ˇfoo)"};
2974        cx.set_shared_state(initial_state).await;
2975        cx.simulate_shared_keystrokes("}").await;
2976        cx.shared_state().await.assert_eq("something(fooˇ)");
2977    }
2978
2979    #[gpui::test]
2980    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2981        let mut cx = NeovimBackedTestContext::new(cx).await;
2982        cx.set_shared_state("ˇone\n  two\nthree").await;
2983        cx.simulate_shared_keystrokes("enter").await;
2984        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
2985    }
2986
2987    #[gpui::test]
2988    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2989        let mut cx = NeovimBackedTestContext::new(cx).await;
2990        cx.set_shared_state("ˇ one\n two \nthree").await;
2991        cx.simulate_shared_keystrokes("g _").await;
2992        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2993
2994        cx.set_shared_state("ˇ one \n two \nthree").await;
2995        cx.simulate_shared_keystrokes("g _").await;
2996        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2997        cx.simulate_shared_keystrokes("2 g _").await;
2998        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2999    }
3000
3001    #[gpui::test]
3002    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3003        let mut cx = NeovimBackedTestContext::new(cx).await;
3004        let initial_state = indoc! {r"abc
3005          def
3006          paragraph
3007          the second
3008          third ˇand
3009          final"};
3010
3011        cx.set_shared_state(initial_state).await;
3012        cx.simulate_shared_keystrokes("shift-h").await;
3013        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3014          def
3015          paragraph
3016          the second
3017          third and
3018          final"});
3019
3020        // clip point
3021        cx.set_shared_state(indoc! {r"
3022          1 2 3
3023          4 5 6
3024          7 8 ˇ9
3025          "})
3026            .await;
3027        cx.simulate_shared_keystrokes("shift-h").await;
3028        cx.shared_state().await.assert_eq(indoc! {"
3029          1 2 ˇ3
3030          4 5 6
3031          7 8 9
3032          "});
3033
3034        cx.set_shared_state(indoc! {r"
3035          1 2 3
3036          4 5 6
3037          ˇ7 8 9
3038          "})
3039            .await;
3040        cx.simulate_shared_keystrokes("shift-h").await;
3041        cx.shared_state().await.assert_eq(indoc! {"
3042          ˇ1 2 3
3043          4 5 6
3044          7 8 9
3045          "});
3046
3047        cx.set_shared_state(indoc! {r"
3048          1 2 3
3049          4 5 ˇ6
3050          7 8 9"})
3051            .await;
3052        cx.simulate_shared_keystrokes("9 shift-h").await;
3053        cx.shared_state().await.assert_eq(indoc! {"
3054          1 2 3
3055          4 5 6
3056          7 8 ˇ9"});
3057    }
3058
3059    #[gpui::test]
3060    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3061        let mut cx = NeovimBackedTestContext::new(cx).await;
3062        let initial_state = indoc! {r"abˇc
3063          def
3064          paragraph
3065          the second
3066          third and
3067          final"};
3068
3069        cx.set_shared_state(initial_state).await;
3070        cx.simulate_shared_keystrokes("shift-m").await;
3071        cx.shared_state().await.assert_eq(indoc! {r"abc
3072          def
3073          paˇragraph
3074          the second
3075          third and
3076          final"});
3077
3078        cx.set_shared_state(indoc! {r"
3079          1 2 3
3080          4 5 6
3081          7 8 ˇ9
3082          "})
3083            .await;
3084        cx.simulate_shared_keystrokes("shift-m").await;
3085        cx.shared_state().await.assert_eq(indoc! {"
3086          1 2 3
3087          4 5 ˇ6
3088          7 8 9
3089          "});
3090        cx.set_shared_state(indoc! {r"
3091          1 2 3
3092          4 5 6
3093          ˇ7 8 9
3094          "})
3095            .await;
3096        cx.simulate_shared_keystrokes("shift-m").await;
3097        cx.shared_state().await.assert_eq(indoc! {"
3098          1 2 3
3099          ˇ4 5 6
3100          7 8 9
3101          "});
3102        cx.set_shared_state(indoc! {r"
3103          ˇ1 2 3
3104          4 5 6
3105          7 8 9
3106          "})
3107            .await;
3108        cx.simulate_shared_keystrokes("shift-m").await;
3109        cx.shared_state().await.assert_eq(indoc! {"
3110          1 2 3
3111          ˇ4 5 6
3112          7 8 9
3113          "});
3114        cx.set_shared_state(indoc! {r"
3115          1 2 3
3116          ˇ4 5 6
3117          7 8 9
3118          "})
3119            .await;
3120        cx.simulate_shared_keystrokes("shift-m").await;
3121        cx.shared_state().await.assert_eq(indoc! {"
3122          1 2 3
3123          ˇ4 5 6
3124          7 8 9
3125          "});
3126        cx.set_shared_state(indoc! {r"
3127          1 2 3
3128          4 5 ˇ6
3129          7 8 9
3130          "})
3131            .await;
3132        cx.simulate_shared_keystrokes("shift-m").await;
3133        cx.shared_state().await.assert_eq(indoc! {"
3134          1 2 3
3135          4 5 ˇ6
3136          7 8 9
3137          "});
3138    }
3139
3140    #[gpui::test]
3141    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3142        let mut cx = NeovimBackedTestContext::new(cx).await;
3143        let initial_state = indoc! {r"abc
3144          deˇf
3145          paragraph
3146          the second
3147          third and
3148          final"};
3149
3150        cx.set_shared_state(initial_state).await;
3151        cx.simulate_shared_keystrokes("shift-l").await;
3152        cx.shared_state().await.assert_eq(indoc! {r"abc
3153          def
3154          paragraph
3155          the second
3156          third and
3157          fiˇnal"});
3158
3159        cx.set_shared_state(indoc! {r"
3160          1 2 3
3161          4 5 ˇ6
3162          7 8 9
3163          "})
3164            .await;
3165        cx.simulate_shared_keystrokes("shift-l").await;
3166        cx.shared_state().await.assert_eq(indoc! {"
3167          1 2 3
3168          4 5 6
3169          7 8 9
3170          ˇ"});
3171
3172        cx.set_shared_state(indoc! {r"
3173          1 2 3
3174          ˇ4 5 6
3175          7 8 9
3176          "})
3177            .await;
3178        cx.simulate_shared_keystrokes("shift-l").await;
3179        cx.shared_state().await.assert_eq(indoc! {"
3180          1 2 3
3181          4 5 6
3182          7 8 9
3183          ˇ"});
3184
3185        cx.set_shared_state(indoc! {r"
3186          1 2 ˇ3
3187          4 5 6
3188          7 8 9
3189          "})
3190            .await;
3191        cx.simulate_shared_keystrokes("shift-l").await;
3192        cx.shared_state().await.assert_eq(indoc! {"
3193          1 2 3
3194          4 5 6
3195          7 8 9
3196          ˇ"});
3197
3198        cx.set_shared_state(indoc! {r"
3199          ˇ1 2 3
3200          4 5 6
3201          7 8 9
3202          "})
3203            .await;
3204        cx.simulate_shared_keystrokes("shift-l").await;
3205        cx.shared_state().await.assert_eq(indoc! {"
3206          1 2 3
3207          4 5 6
3208          7 8 9
3209          ˇ"});
3210
3211        cx.set_shared_state(indoc! {r"
3212          1 2 3
3213          4 5 ˇ6
3214          7 8 9
3215          "})
3216            .await;
3217        cx.simulate_shared_keystrokes("9 shift-l").await;
3218        cx.shared_state().await.assert_eq(indoc! {"
3219          1 2 ˇ3
3220          4 5 6
3221          7 8 9
3222          "});
3223    }
3224
3225    #[gpui::test]
3226    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3227        let mut cx = NeovimBackedTestContext::new(cx).await;
3228        cx.set_shared_state(indoc! {r"
3229        456 5ˇ67 678
3230        "})
3231            .await;
3232        cx.simulate_shared_keystrokes("g e").await;
3233        cx.shared_state().await.assert_eq(indoc! {"
3234        45ˇ6 567 678
3235        "});
3236
3237        // Test times
3238        cx.set_shared_state(indoc! {r"
3239        123 234 345
3240        456 5ˇ67 678
3241        "})
3242            .await;
3243        cx.simulate_shared_keystrokes("4 g e").await;
3244        cx.shared_state().await.assert_eq(indoc! {"
3245        12ˇ3 234 345
3246        456 567 678
3247        "});
3248
3249        // With punctuation
3250        cx.set_shared_state(indoc! {r"
3251        123 234 345
3252        4;5.6 5ˇ67 678
3253        789 890 901
3254        "})
3255            .await;
3256        cx.simulate_shared_keystrokes("g e").await;
3257        cx.shared_state().await.assert_eq(indoc! {"
3258          123 234 345
3259          4;5.ˇ6 567 678
3260          789 890 901
3261        "});
3262
3263        // With punctuation and count
3264        cx.set_shared_state(indoc! {r"
3265        123 234 345
3266        4;5.6 5ˇ67 678
3267        789 890 901
3268        "})
3269            .await;
3270        cx.simulate_shared_keystrokes("5 g e").await;
3271        cx.shared_state().await.assert_eq(indoc! {"
3272          123 234 345
3273          ˇ4;5.6 567 678
3274          789 890 901
3275        "});
3276
3277        // newlines
3278        cx.set_shared_state(indoc! {r"
3279        123 234 345
3280
3281        78ˇ9 890 901
3282        "})
3283            .await;
3284        cx.simulate_shared_keystrokes("g e").await;
3285        cx.shared_state().await.assert_eq(indoc! {"
3286          123 234 345
3287          ˇ
3288          789 890 901
3289        "});
3290        cx.simulate_shared_keystrokes("g e").await;
3291        cx.shared_state().await.assert_eq(indoc! {"
3292          123 234 34ˇ5
3293
3294          789 890 901
3295        "});
3296
3297        // With punctuation
3298        cx.set_shared_state(indoc! {r"
3299        123 234 345
3300        4;5.ˇ6 567 678
3301        789 890 901
3302        "})
3303            .await;
3304        cx.simulate_shared_keystrokes("g shift-e").await;
3305        cx.shared_state().await.assert_eq(indoc! {"
3306          123 234 34ˇ5
3307          4;5.6 567 678
3308          789 890 901
3309        "});
3310    }
3311
3312    #[gpui::test]
3313    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3314        let mut cx = NeovimBackedTestContext::new(cx).await;
3315
3316        cx.set_shared_state(indoc! {"
3317            fn aˇ() {
3318              return
3319            }
3320        "})
3321            .await;
3322        cx.simulate_shared_keystrokes("v $ %").await;
3323        cx.shared_state().await.assert_eq(indoc! {"
3324            fn a«() {
3325              return
3326            }ˇ»
3327        "});
3328    }
3329
3330    #[gpui::test]
3331    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3332        let mut cx = VimTestContext::new(cx, true).await;
3333
3334        cx.set_state(
3335            indoc! {"
3336            struct Foo {
3337            ˇ
3338            }
3339        "},
3340            Mode::Normal,
3341        );
3342
3343        cx.update_editor(|editor, cx| {
3344            let range = editor.selections.newest_anchor().range();
3345            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3346            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3347            editor.splice_inlays(vec![], vec![inlay], cx);
3348        });
3349
3350        cx.simulate_keystrokes("j");
3351        cx.assert_state(
3352            indoc! {"
3353            struct Foo {
3354
3355            ˇ}
3356        "},
3357            Mode::Normal,
3358        );
3359    }
3360}