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.start >= offset && line_range.contains(&open_range.start) {
2047                let distance = open_range.start - offset;
2048                if distance < closest_distance {
2049                    closest_pair_destination = Some(close_range.end - 1);
2050                    closest_distance = distance;
2051                    continue;
2052                }
2053            }
2054
2055            if close_range.start >= offset && line_range.contains(&close_range.start) {
2056                let distance = close_range.start - offset;
2057                if distance < closest_distance {
2058                    closest_pair_destination = Some(open_range.start);
2059                    closest_distance = distance;
2060                    continue;
2061                }
2062            }
2063
2064            continue;
2065        }
2066
2067        closest_pair_destination
2068            .map(|destination| destination.to_display_point(map))
2069            .unwrap_or(display_point)
2070    } else {
2071        display_point
2072    }
2073}
2074
2075fn unmatched_forward(
2076    map: &DisplaySnapshot,
2077    mut display_point: DisplayPoint,
2078    char: char,
2079    times: usize,
2080) -> DisplayPoint {
2081    for _ in 0..times {
2082        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2083        let point = display_point.to_point(map);
2084        let offset = point.to_offset(&map.buffer_snapshot);
2085
2086        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2087        let Some(ranges) = ranges else { break };
2088        let mut closest_closing_destination = None;
2089        let mut closest_distance = usize::MAX;
2090
2091        for (_, close_range) in ranges {
2092            if close_range.start > offset {
2093                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2094                if Some(char) == chars.next() {
2095                    let distance = close_range.start - offset;
2096                    if distance < closest_distance {
2097                        closest_closing_destination = Some(close_range.start);
2098                        closest_distance = distance;
2099                        continue;
2100                    }
2101                }
2102            }
2103        }
2104
2105        let new_point = closest_closing_destination
2106            .map(|destination| destination.to_display_point(map))
2107            .unwrap_or(display_point);
2108        if new_point == display_point {
2109            break;
2110        }
2111        display_point = new_point;
2112    }
2113    return display_point;
2114}
2115
2116fn unmatched_backward(
2117    map: &DisplaySnapshot,
2118    mut display_point: DisplayPoint,
2119    char: char,
2120    times: usize,
2121) -> DisplayPoint {
2122    for _ in 0..times {
2123        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2124        let point = display_point.to_point(map);
2125        let offset = point.to_offset(&map.buffer_snapshot);
2126
2127        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2128        let Some(ranges) = ranges else {
2129            break;
2130        };
2131
2132        let mut closest_starting_destination = None;
2133        let mut closest_distance = usize::MAX;
2134
2135        for (start_range, _) in ranges {
2136            if start_range.start < offset {
2137                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2138                if Some(char) == chars.next() {
2139                    let distance = offset - start_range.start;
2140                    if distance < closest_distance {
2141                        closest_starting_destination = Some(start_range.start);
2142                        closest_distance = distance;
2143                        continue;
2144                    }
2145                }
2146            }
2147        }
2148
2149        let new_point = closest_starting_destination
2150            .map(|destination| destination.to_display_point(map))
2151            .unwrap_or(display_point);
2152        if new_point == display_point {
2153            break;
2154        } else {
2155            display_point = new_point;
2156        }
2157    }
2158    display_point
2159}
2160
2161fn find_forward(
2162    map: &DisplaySnapshot,
2163    from: DisplayPoint,
2164    before: bool,
2165    target: char,
2166    times: usize,
2167    mode: FindRange,
2168    smartcase: bool,
2169) -> Option<DisplayPoint> {
2170    let mut to = from;
2171    let mut found = false;
2172
2173    for _ in 0..times {
2174        found = false;
2175        let new_to = find_boundary(map, to, mode, |_, right| {
2176            found = is_character_match(target, right, smartcase);
2177            found
2178        });
2179        if to == new_to {
2180            break;
2181        }
2182        to = new_to;
2183    }
2184
2185    if found {
2186        if before && to.column() > 0 {
2187            *to.column_mut() -= 1;
2188            Some(map.clip_point(to, Bias::Left))
2189        } else {
2190            Some(to)
2191        }
2192    } else {
2193        None
2194    }
2195}
2196
2197fn find_backward(
2198    map: &DisplaySnapshot,
2199    from: DisplayPoint,
2200    after: bool,
2201    target: char,
2202    times: usize,
2203    mode: FindRange,
2204    smartcase: bool,
2205) -> DisplayPoint {
2206    let mut to = from;
2207
2208    for _ in 0..times {
2209        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2210            is_character_match(target, right, smartcase)
2211        });
2212        if to == new_to {
2213            break;
2214        }
2215        to = new_to;
2216    }
2217
2218    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2219    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2220        if after {
2221            *to.column_mut() += 1;
2222            map.clip_point(to, Bias::Right)
2223        } else {
2224            to
2225        }
2226    } else {
2227        from
2228    }
2229}
2230
2231fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2232    if smartcase {
2233        if target.is_uppercase() {
2234            target == other
2235        } else {
2236            target == other.to_ascii_lowercase()
2237        }
2238    } else {
2239        target == other
2240    }
2241}
2242
2243fn sneak(
2244    map: &DisplaySnapshot,
2245    from: DisplayPoint,
2246    first_target: char,
2247    second_target: char,
2248    times: usize,
2249    smartcase: bool,
2250) -> Option<DisplayPoint> {
2251    let mut to = from;
2252    let mut found = false;
2253
2254    for _ in 0..times {
2255        found = false;
2256        let new_to = find_boundary(
2257            map,
2258            movement::right(map, to),
2259            FindRange::MultiLine,
2260            |left, right| {
2261                found = is_character_match(first_target, left, smartcase)
2262                    && is_character_match(second_target, right, smartcase);
2263                found
2264            },
2265        );
2266        if to == new_to {
2267            break;
2268        }
2269        to = new_to;
2270    }
2271
2272    if found {
2273        Some(movement::left(map, to))
2274    } else {
2275        None
2276    }
2277}
2278
2279fn sneak_backward(
2280    map: &DisplaySnapshot,
2281    from: DisplayPoint,
2282    first_target: char,
2283    second_target: char,
2284    times: usize,
2285    smartcase: bool,
2286) -> Option<DisplayPoint> {
2287    let mut to = from;
2288    let mut found = false;
2289
2290    for _ in 0..times {
2291        found = false;
2292        let new_to =
2293            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2294                found = is_character_match(first_target, left, smartcase)
2295                    && is_character_match(second_target, right, smartcase);
2296                found
2297            });
2298        if to == new_to {
2299            break;
2300        }
2301        to = new_to;
2302    }
2303
2304    if found {
2305        Some(movement::left(map, to))
2306    } else {
2307        None
2308    }
2309}
2310
2311fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2312    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2313    first_non_whitespace(map, false, correct_line)
2314}
2315
2316fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2317    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2318    first_non_whitespace(map, false, correct_line)
2319}
2320
2321fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2322    let correct_line = start_of_relative_buffer_row(map, point, 0);
2323    right(map, correct_line, times.saturating_sub(1))
2324}
2325
2326pub(crate) fn next_line_end(
2327    map: &DisplaySnapshot,
2328    mut point: DisplayPoint,
2329    times: usize,
2330) -> DisplayPoint {
2331    if times > 1 {
2332        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2333    }
2334    end_of_line(map, false, point, 1)
2335}
2336
2337fn window_top(
2338    map: &DisplaySnapshot,
2339    point: DisplayPoint,
2340    text_layout_details: &TextLayoutDetails,
2341    mut times: usize,
2342) -> (DisplayPoint, SelectionGoal) {
2343    let first_visible_line = text_layout_details
2344        .scroll_anchor
2345        .anchor
2346        .to_display_point(map);
2347
2348    if first_visible_line.row() != DisplayRow(0)
2349        && text_layout_details.vertical_scroll_margin as usize > times
2350    {
2351        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2352    }
2353
2354    if let Some(visible_rows) = text_layout_details.visible_rows {
2355        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2356        let new_row = (first_visible_line.row().0 + (times as u32))
2357            .min(bottom_row)
2358            .min(map.max_point().row().0);
2359        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2360
2361        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2362        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2363    } else {
2364        let new_row =
2365            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2366        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2367
2368        let new_point = DisplayPoint::new(new_row, new_col);
2369        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2370    }
2371}
2372
2373fn window_middle(
2374    map: &DisplaySnapshot,
2375    point: DisplayPoint,
2376    text_layout_details: &TextLayoutDetails,
2377) -> (DisplayPoint, SelectionGoal) {
2378    if let Some(visible_rows) = text_layout_details.visible_rows {
2379        let first_visible_line = text_layout_details
2380            .scroll_anchor
2381            .anchor
2382            .to_display_point(map);
2383
2384        let max_visible_rows =
2385            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2386
2387        let new_row =
2388            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2389        let new_row = DisplayRow(new_row);
2390        let new_col = point.column().min(map.line_len(new_row));
2391        let new_point = DisplayPoint::new(new_row, new_col);
2392        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2393    } else {
2394        (point, SelectionGoal::None)
2395    }
2396}
2397
2398fn window_bottom(
2399    map: &DisplaySnapshot,
2400    point: DisplayPoint,
2401    text_layout_details: &TextLayoutDetails,
2402    mut times: usize,
2403) -> (DisplayPoint, SelectionGoal) {
2404    if let Some(visible_rows) = text_layout_details.visible_rows {
2405        let first_visible_line = text_layout_details
2406            .scroll_anchor
2407            .anchor
2408            .to_display_point(map);
2409        let bottom_row = first_visible_line.row().0
2410            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2411        if bottom_row < map.max_point().row().0
2412            && text_layout_details.vertical_scroll_margin as usize > times
2413        {
2414            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2415        }
2416        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2417        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2418        {
2419            first_visible_line.row()
2420        } else {
2421            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2422        };
2423        let new_col = point.column().min(map.line_len(new_row));
2424        let new_point = DisplayPoint::new(new_row, new_col);
2425        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2426    } else {
2427        (point, SelectionGoal::None)
2428    }
2429}
2430
2431fn method_motion(
2432    map: &DisplaySnapshot,
2433    mut display_point: DisplayPoint,
2434    times: usize,
2435    direction: Direction,
2436    is_start: bool,
2437) -> DisplayPoint {
2438    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2439        return display_point;
2440    };
2441
2442    for _ in 0..times {
2443        let point = map.display_point_to_point(display_point, Bias::Left);
2444        let offset = point.to_offset(&map.buffer_snapshot);
2445        let range = if direction == Direction::Prev {
2446            0..offset
2447        } else {
2448            offset..buffer.len()
2449        };
2450
2451        let possibilities = buffer
2452            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2453            .filter_map(|(range, object)| {
2454                if !matches!(object, language::TextObject::AroundFunction) {
2455                    return None;
2456                }
2457
2458                let relevant = if is_start { range.start } else { range.end };
2459                if direction == Direction::Prev && relevant < offset {
2460                    Some(relevant)
2461                } else if direction == Direction::Next && relevant > offset + 1 {
2462                    Some(relevant)
2463                } else {
2464                    None
2465                }
2466            });
2467
2468        let dest = if direction == Direction::Prev {
2469            possibilities.max().unwrap_or(offset)
2470        } else {
2471            possibilities.min().unwrap_or(offset)
2472        };
2473        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2474        if new_point == display_point {
2475            break;
2476        }
2477        display_point = new_point;
2478    }
2479    display_point
2480}
2481
2482fn comment_motion(
2483    map: &DisplaySnapshot,
2484    mut display_point: DisplayPoint,
2485    times: usize,
2486    direction: Direction,
2487) -> DisplayPoint {
2488    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2489        return display_point;
2490    };
2491
2492    for _ in 0..times {
2493        let point = map.display_point_to_point(display_point, Bias::Left);
2494        let offset = point.to_offset(&map.buffer_snapshot);
2495        let range = if direction == Direction::Prev {
2496            0..offset
2497        } else {
2498            offset..buffer.len()
2499        };
2500
2501        let possibilities = buffer
2502            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2503            .filter_map(|(range, object)| {
2504                if !matches!(object, language::TextObject::AroundComment) {
2505                    return None;
2506                }
2507
2508                let relevant = if direction == Direction::Prev {
2509                    range.start
2510                } else {
2511                    range.end
2512                };
2513                if direction == Direction::Prev && relevant < offset {
2514                    Some(relevant)
2515                } else if direction == Direction::Next && relevant > offset + 1 {
2516                    Some(relevant)
2517                } else {
2518                    None
2519                }
2520            });
2521
2522        let dest = if direction == Direction::Prev {
2523            possibilities.max().unwrap_or(offset)
2524        } else {
2525            possibilities.min().unwrap_or(offset)
2526        };
2527        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2528        if new_point == display_point {
2529            break;
2530        }
2531        display_point = new_point;
2532    }
2533
2534    display_point
2535}
2536
2537fn section_motion(
2538    map: &DisplaySnapshot,
2539    mut display_point: DisplayPoint,
2540    times: usize,
2541    direction: Direction,
2542    is_start: bool,
2543) -> DisplayPoint {
2544    if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
2545        for _ in 0..times {
2546            let offset = map
2547                .display_point_to_point(display_point, Bias::Left)
2548                .to_offset(&map.buffer_snapshot);
2549            let range = if direction == Direction::Prev {
2550                0..offset
2551            } else {
2552                offset..buffer.len()
2553            };
2554
2555            // we set a max start depth here because we want a section to only be "top level"
2556            // similar to vim's default of '{' in the first column.
2557            // (and without it, ]] at the start of editor.rs is -very- slow)
2558            let mut possibilities = buffer
2559                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2560                .filter(|(_, object)| {
2561                    matches!(
2562                        object,
2563                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2564                    )
2565                })
2566                .collect::<Vec<_>>();
2567            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2568            let mut prev_end = None;
2569            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2570                if t == language::TextObject::AroundFunction
2571                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2572                {
2573                    return None;
2574                }
2575                prev_end = Some(range.end);
2576
2577                let relevant = if is_start { range.start } else { range.end };
2578                if direction == Direction::Prev && relevant < offset {
2579                    Some(relevant)
2580                } else if direction == Direction::Next && relevant > offset + 1 {
2581                    Some(relevant)
2582                } else {
2583                    None
2584                }
2585            });
2586
2587            let offset = if direction == Direction::Prev {
2588                possibilities.max().unwrap_or(0)
2589            } else {
2590                possibilities.min().unwrap_or(buffer.len())
2591            };
2592
2593            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2594            if new_point == display_point {
2595                break;
2596            }
2597            display_point = new_point;
2598        }
2599        return display_point;
2600    };
2601
2602    for _ in 0..times {
2603        let point = map.display_point_to_point(display_point, Bias::Left);
2604        let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2605            return display_point;
2606        };
2607        let next_point = match (direction, is_start) {
2608            (Direction::Prev, true) => {
2609                let mut start = excerpt.start_anchor().to_display_point(&map);
2610                if start >= display_point && start.row() > DisplayRow(0) {
2611                    let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2612                        return display_point;
2613                    };
2614                    start = excerpt.start_anchor().to_display_point(&map);
2615                }
2616                start
2617            }
2618            (Direction::Prev, false) => {
2619                let mut start = excerpt.start_anchor().to_display_point(&map);
2620                if start.row() > DisplayRow(0) {
2621                    *start.row_mut() -= 1;
2622                }
2623                map.clip_point(start, Bias::Left)
2624            }
2625            (Direction::Next, true) => {
2626                let mut end = excerpt.end_anchor().to_display_point(&map);
2627                *end.row_mut() += 1;
2628                map.clip_point(end, Bias::Right)
2629            }
2630            (Direction::Next, false) => {
2631                let mut end = excerpt.end_anchor().to_display_point(&map);
2632                *end.column_mut() = 0;
2633                if end <= display_point {
2634                    *end.row_mut() += 1;
2635                    let point_end = map.display_point_to_point(end, Bias::Right);
2636                    let Some(excerpt) =
2637                        map.buffer_snapshot.excerpt_containing(point_end..point_end)
2638                    else {
2639                        return display_point;
2640                    };
2641                    end = excerpt.end_anchor().to_display_point(&map);
2642                    *end.column_mut() = 0;
2643                }
2644                end
2645            }
2646        };
2647        if next_point == display_point {
2648            break;
2649        }
2650        display_point = next_point;
2651    }
2652
2653    display_point
2654}
2655
2656#[cfg(test)]
2657mod test {
2658
2659    use crate::{
2660        state::Mode,
2661        test::{NeovimBackedTestContext, VimTestContext},
2662    };
2663    use editor::display_map::Inlay;
2664    use indoc::indoc;
2665
2666    #[gpui::test]
2667    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2668        let mut cx = NeovimBackedTestContext::new(cx).await;
2669
2670        let initial_state = indoc! {r"ˇabc
2671            def
2672
2673            paragraph
2674            the second
2675
2676
2677
2678            third and
2679            final"};
2680
2681        // goes down once
2682        cx.set_shared_state(initial_state).await;
2683        cx.simulate_shared_keystrokes("}").await;
2684        cx.shared_state().await.assert_eq(indoc! {r"abc
2685            def
2686            ˇ
2687            paragraph
2688            the second
2689
2690
2691
2692            third and
2693            final"});
2694
2695        // goes up once
2696        cx.simulate_shared_keystrokes("{").await;
2697        cx.shared_state().await.assert_eq(initial_state);
2698
2699        // goes down twice
2700        cx.simulate_shared_keystrokes("2 }").await;
2701        cx.shared_state().await.assert_eq(indoc! {r"abc
2702            def
2703
2704            paragraph
2705            the second
2706            ˇ
2707
2708
2709            third and
2710            final"});
2711
2712        // goes down over multiple blanks
2713        cx.simulate_shared_keystrokes("}").await;
2714        cx.shared_state().await.assert_eq(indoc! {r"abc
2715                def
2716
2717                paragraph
2718                the second
2719
2720
2721
2722                third and
2723                finaˇl"});
2724
2725        // goes up twice
2726        cx.simulate_shared_keystrokes("2 {").await;
2727        cx.shared_state().await.assert_eq(indoc! {r"abc
2728                def
2729                ˇ
2730                paragraph
2731                the second
2732
2733
2734
2735                third and
2736                final"});
2737    }
2738
2739    #[gpui::test]
2740    async fn test_matching(cx: &mut gpui::TestAppContext) {
2741        let mut cx = NeovimBackedTestContext::new(cx).await;
2742
2743        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2744                do(something(with<Types>.and_arrays[0, 2]))
2745            }"})
2746            .await;
2747        cx.simulate_shared_keystrokes("%").await;
2748        cx.shared_state()
2749            .await
2750            .assert_eq(indoc! {r"func (a stringˇ) {
2751                do(something(with<Types>.and_arrays[0, 2]))
2752            }"});
2753
2754        // test it works on the last character of the line
2755        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2756            do(something(with<Types>.and_arrays[0, 2]))
2757            }"})
2758            .await;
2759        cx.simulate_shared_keystrokes("%").await;
2760        cx.shared_state()
2761            .await
2762            .assert_eq(indoc! {r"func (a string) {
2763            do(something(with<Types>.and_arrays[0, 2]))
2764            ˇ}"});
2765
2766        // test it works on immediate nesting
2767        cx.set_shared_state("ˇ{()}").await;
2768        cx.simulate_shared_keystrokes("%").await;
2769        cx.shared_state().await.assert_eq("{()ˇ}");
2770        cx.simulate_shared_keystrokes("%").await;
2771        cx.shared_state().await.assert_eq("ˇ{()}");
2772
2773        // test it works on immediate nesting inside braces
2774        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2775        cx.simulate_shared_keystrokes("%").await;
2776        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2777
2778        // test it jumps to the next paren on a line
2779        cx.set_shared_state("func ˇboop() {\n}").await;
2780        cx.simulate_shared_keystrokes("%").await;
2781        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2782    }
2783
2784    #[gpui::test]
2785    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2786        let mut cx = NeovimBackedTestContext::new(cx).await;
2787
2788        // test it works with curly braces
2789        cx.set_shared_state(indoc! {r"func (a string) {
2790                do(something(with<Types>.anˇd_arrays[0, 2]))
2791            }"})
2792            .await;
2793        cx.simulate_shared_keystrokes("] }").await;
2794        cx.shared_state()
2795            .await
2796            .assert_eq(indoc! {r"func (a string) {
2797                do(something(with<Types>.and_arrays[0, 2]))
2798            ˇ}"});
2799
2800        // test it works with brackets
2801        cx.set_shared_state(indoc! {r"func (a string) {
2802                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2803            }"})
2804            .await;
2805        cx.simulate_shared_keystrokes("] )").await;
2806        cx.shared_state()
2807            .await
2808            .assert_eq(indoc! {r"func (a string) {
2809                do(something(with<Types>.and_arrays[0, 2])ˇ)
2810            }"});
2811
2812        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2813            .await;
2814        cx.simulate_shared_keystrokes("] )").await;
2815        cx.shared_state()
2816            .await
2817            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2818
2819        // test it works on immediate nesting
2820        cx.set_shared_state("{ˇ {}{}}").await;
2821        cx.simulate_shared_keystrokes("] }").await;
2822        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2823        cx.set_shared_state("(ˇ ()())").await;
2824        cx.simulate_shared_keystrokes("] )").await;
2825        cx.shared_state().await.assert_eq("( ()()ˇ)");
2826
2827        // test it works on immediate nesting inside braces
2828        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2829        cx.simulate_shared_keystrokes("] }").await;
2830        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2831        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2832        cx.simulate_shared_keystrokes("] )").await;
2833        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2834    }
2835
2836    #[gpui::test]
2837    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2838        let mut cx = NeovimBackedTestContext::new(cx).await;
2839
2840        // test it works with curly braces
2841        cx.set_shared_state(indoc! {r"func (a string) {
2842                do(something(with<Types>.anˇd_arrays[0, 2]))
2843            }"})
2844            .await;
2845        cx.simulate_shared_keystrokes("[ {").await;
2846        cx.shared_state()
2847            .await
2848            .assert_eq(indoc! {r"func (a string) ˇ{
2849                do(something(with<Types>.and_arrays[0, 2]))
2850            }"});
2851
2852        // test it works with brackets
2853        cx.set_shared_state(indoc! {r"func (a string) {
2854                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2855            }"})
2856            .await;
2857        cx.simulate_shared_keystrokes("[ (").await;
2858        cx.shared_state()
2859            .await
2860            .assert_eq(indoc! {r"func (a string) {
2861                doˇ(something(with<Types>.and_arrays[0, 2]))
2862            }"});
2863
2864        // test it works on immediate nesting
2865        cx.set_shared_state("{{}{} ˇ }").await;
2866        cx.simulate_shared_keystrokes("[ {").await;
2867        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2868        cx.set_shared_state("(()() ˇ )").await;
2869        cx.simulate_shared_keystrokes("[ (").await;
2870        cx.shared_state().await.assert_eq("ˇ(()()  )");
2871
2872        // test it works on immediate nesting inside braces
2873        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2874        cx.simulate_shared_keystrokes("[ {").await;
2875        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2876        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2877        cx.simulate_shared_keystrokes("[ (").await;
2878        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2879    }
2880
2881    #[gpui::test]
2882    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2883        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2884
2885        cx.neovim.exec("set filetype=html").await;
2886
2887        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2888        cx.simulate_shared_keystrokes("%").await;
2889        cx.shared_state()
2890            .await
2891            .assert_eq(indoc! {r"<body><ˇ/body>"});
2892        cx.simulate_shared_keystrokes("%").await;
2893
2894        // test jumping backwards
2895        cx.shared_state()
2896            .await
2897            .assert_eq(indoc! {r"<ˇbody></body>"});
2898
2899        // test self-closing tags
2900        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2901        cx.simulate_shared_keystrokes("%").await;
2902        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2903
2904        // test tag with attributes
2905        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2906            </div>
2907            "})
2908            .await;
2909        cx.simulate_shared_keystrokes("%").await;
2910        cx.shared_state()
2911            .await
2912            .assert_eq(indoc! {r"<div class='test' id='main'>
2913            <ˇ/div>
2914            "});
2915
2916        // test multi-line self-closing tag
2917        cx.set_shared_state(indoc! {r#"<a>
2918            <br
2919                test = "test"
2920            /ˇ>
2921        </a>"#})
2922            .await;
2923        cx.simulate_shared_keystrokes("%").await;
2924        cx.shared_state().await.assert_eq(indoc! {r#"<a>
2925            ˇ<br
2926                test = "test"
2927            />
2928        </a>"#});
2929    }
2930
2931    #[gpui::test]
2932    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2933        let mut cx = NeovimBackedTestContext::new(cx).await;
2934
2935        // f and F
2936        cx.set_shared_state("ˇone two three four").await;
2937        cx.simulate_shared_keystrokes("f o").await;
2938        cx.shared_state().await.assert_eq("one twˇo three four");
2939        cx.simulate_shared_keystrokes(",").await;
2940        cx.shared_state().await.assert_eq("ˇone two three four");
2941        cx.simulate_shared_keystrokes("2 ;").await;
2942        cx.shared_state().await.assert_eq("one two three fˇour");
2943        cx.simulate_shared_keystrokes("shift-f e").await;
2944        cx.shared_state().await.assert_eq("one two threˇe four");
2945        cx.simulate_shared_keystrokes("2 ;").await;
2946        cx.shared_state().await.assert_eq("onˇe two three four");
2947        cx.simulate_shared_keystrokes(",").await;
2948        cx.shared_state().await.assert_eq("one two thrˇee four");
2949
2950        // t and T
2951        cx.set_shared_state("ˇone two three four").await;
2952        cx.simulate_shared_keystrokes("t o").await;
2953        cx.shared_state().await.assert_eq("one tˇwo three four");
2954        cx.simulate_shared_keystrokes(",").await;
2955        cx.shared_state().await.assert_eq("oˇne two three four");
2956        cx.simulate_shared_keystrokes("2 ;").await;
2957        cx.shared_state().await.assert_eq("one two three ˇfour");
2958        cx.simulate_shared_keystrokes("shift-t e").await;
2959        cx.shared_state().await.assert_eq("one two threeˇ four");
2960        cx.simulate_shared_keystrokes("3 ;").await;
2961        cx.shared_state().await.assert_eq("oneˇ two three four");
2962        cx.simulate_shared_keystrokes(",").await;
2963        cx.shared_state().await.assert_eq("one two thˇree four");
2964    }
2965
2966    #[gpui::test]
2967    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2968        let mut cx = NeovimBackedTestContext::new(cx).await;
2969        let initial_state = indoc! {r"something(ˇfoo)"};
2970        cx.set_shared_state(initial_state).await;
2971        cx.simulate_shared_keystrokes("}").await;
2972        cx.shared_state().await.assert_eq("something(fooˇ)");
2973    }
2974
2975    #[gpui::test]
2976    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2977        let mut cx = NeovimBackedTestContext::new(cx).await;
2978        cx.set_shared_state("ˇone\n  two\nthree").await;
2979        cx.simulate_shared_keystrokes("enter").await;
2980        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
2981    }
2982
2983    #[gpui::test]
2984    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2985        let mut cx = NeovimBackedTestContext::new(cx).await;
2986        cx.set_shared_state("ˇ one\n two \nthree").await;
2987        cx.simulate_shared_keystrokes("g _").await;
2988        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2989
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        cx.simulate_shared_keystrokes("2 g _").await;
2994        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2995    }
2996
2997    #[gpui::test]
2998    async fn test_window_top(cx: &mut gpui::TestAppContext) {
2999        let mut cx = NeovimBackedTestContext::new(cx).await;
3000        let initial_state = indoc! {r"abc
3001          def
3002          paragraph
3003          the second
3004          third ˇand
3005          final"};
3006
3007        cx.set_shared_state(initial_state).await;
3008        cx.simulate_shared_keystrokes("shift-h").await;
3009        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3010          def
3011          paragraph
3012          the second
3013          third and
3014          final"});
3015
3016        // clip point
3017        cx.set_shared_state(indoc! {r"
3018          1 2 3
3019          4 5 6
3020          7 8 ˇ9
3021          "})
3022            .await;
3023        cx.simulate_shared_keystrokes("shift-h").await;
3024        cx.shared_state().await.assert_eq(indoc! {"
3025          1 2 ˇ3
3026          4 5 6
3027          7 8 9
3028          "});
3029
3030        cx.set_shared_state(indoc! {r"
3031          1 2 3
3032          4 5 6
3033          ˇ7 8 9
3034          "})
3035            .await;
3036        cx.simulate_shared_keystrokes("shift-h").await;
3037        cx.shared_state().await.assert_eq(indoc! {"
3038          ˇ1 2 3
3039          4 5 6
3040          7 8 9
3041          "});
3042
3043        cx.set_shared_state(indoc! {r"
3044          1 2 3
3045          4 5 ˇ6
3046          7 8 9"})
3047            .await;
3048        cx.simulate_shared_keystrokes("9 shift-h").await;
3049        cx.shared_state().await.assert_eq(indoc! {"
3050          1 2 3
3051          4 5 6
3052          7 8 ˇ9"});
3053    }
3054
3055    #[gpui::test]
3056    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3057        let mut cx = NeovimBackedTestContext::new(cx).await;
3058        let initial_state = indoc! {r"abˇc
3059          def
3060          paragraph
3061          the second
3062          third and
3063          final"};
3064
3065        cx.set_shared_state(initial_state).await;
3066        cx.simulate_shared_keystrokes("shift-m").await;
3067        cx.shared_state().await.assert_eq(indoc! {r"abc
3068          def
3069          paˇragraph
3070          the second
3071          third and
3072          final"});
3073
3074        cx.set_shared_state(indoc! {r"
3075          1 2 3
3076          4 5 6
3077          7 8 ˇ9
3078          "})
3079            .await;
3080        cx.simulate_shared_keystrokes("shift-m").await;
3081        cx.shared_state().await.assert_eq(indoc! {"
3082          1 2 3
3083          4 5 ˇ6
3084          7 8 9
3085          "});
3086        cx.set_shared_state(indoc! {r"
3087          1 2 3
3088          4 5 6
3089          ˇ7 8 9
3090          "})
3091            .await;
3092        cx.simulate_shared_keystrokes("shift-m").await;
3093        cx.shared_state().await.assert_eq(indoc! {"
3094          1 2 3
3095          ˇ4 5 6
3096          7 8 9
3097          "});
3098        cx.set_shared_state(indoc! {r"
3099          ˇ1 2 3
3100          4 5 6
3101          7 8 9
3102          "})
3103            .await;
3104        cx.simulate_shared_keystrokes("shift-m").await;
3105        cx.shared_state().await.assert_eq(indoc! {"
3106          1 2 3
3107          ˇ4 5 6
3108          7 8 9
3109          "});
3110        cx.set_shared_state(indoc! {r"
3111          1 2 3
3112          ˇ4 5 6
3113          7 8 9
3114          "})
3115            .await;
3116        cx.simulate_shared_keystrokes("shift-m").await;
3117        cx.shared_state().await.assert_eq(indoc! {"
3118          1 2 3
3119          ˇ4 5 6
3120          7 8 9
3121          "});
3122        cx.set_shared_state(indoc! {r"
3123          1 2 3
3124          4 5 ˇ6
3125          7 8 9
3126          "})
3127            .await;
3128        cx.simulate_shared_keystrokes("shift-m").await;
3129        cx.shared_state().await.assert_eq(indoc! {"
3130          1 2 3
3131          4 5 ˇ6
3132          7 8 9
3133          "});
3134    }
3135
3136    #[gpui::test]
3137    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3138        let mut cx = NeovimBackedTestContext::new(cx).await;
3139        let initial_state = indoc! {r"abc
3140          deˇf
3141          paragraph
3142          the second
3143          third and
3144          final"};
3145
3146        cx.set_shared_state(initial_state).await;
3147        cx.simulate_shared_keystrokes("shift-l").await;
3148        cx.shared_state().await.assert_eq(indoc! {r"abc
3149          def
3150          paragraph
3151          the second
3152          third and
3153          fiˇnal"});
3154
3155        cx.set_shared_state(indoc! {r"
3156          1 2 3
3157          4 5 ˇ6
3158          7 8 9
3159          "})
3160            .await;
3161        cx.simulate_shared_keystrokes("shift-l").await;
3162        cx.shared_state().await.assert_eq(indoc! {"
3163          1 2 3
3164          4 5 6
3165          7 8 9
3166          ˇ"});
3167
3168        cx.set_shared_state(indoc! {r"
3169          1 2 3
3170          ˇ4 5 6
3171          7 8 9
3172          "})
3173            .await;
3174        cx.simulate_shared_keystrokes("shift-l").await;
3175        cx.shared_state().await.assert_eq(indoc! {"
3176          1 2 3
3177          4 5 6
3178          7 8 9
3179          ˇ"});
3180
3181        cx.set_shared_state(indoc! {r"
3182          1 2 ˇ3
3183          4 5 6
3184          7 8 9
3185          "})
3186            .await;
3187        cx.simulate_shared_keystrokes("shift-l").await;
3188        cx.shared_state().await.assert_eq(indoc! {"
3189          1 2 3
3190          4 5 6
3191          7 8 9
3192          ˇ"});
3193
3194        cx.set_shared_state(indoc! {r"
3195          ˇ1 2 3
3196          4 5 6
3197          7 8 9
3198          "})
3199            .await;
3200        cx.simulate_shared_keystrokes("shift-l").await;
3201        cx.shared_state().await.assert_eq(indoc! {"
3202          1 2 3
3203          4 5 6
3204          7 8 9
3205          ˇ"});
3206
3207        cx.set_shared_state(indoc! {r"
3208          1 2 3
3209          4 5 ˇ6
3210          7 8 9
3211          "})
3212            .await;
3213        cx.simulate_shared_keystrokes("9 shift-l").await;
3214        cx.shared_state().await.assert_eq(indoc! {"
3215          1 2 ˇ3
3216          4 5 6
3217          7 8 9
3218          "});
3219    }
3220
3221    #[gpui::test]
3222    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3223        let mut cx = NeovimBackedTestContext::new(cx).await;
3224        cx.set_shared_state(indoc! {r"
3225        456 5ˇ67 678
3226        "})
3227            .await;
3228        cx.simulate_shared_keystrokes("g e").await;
3229        cx.shared_state().await.assert_eq(indoc! {"
3230        45ˇ6 567 678
3231        "});
3232
3233        // Test times
3234        cx.set_shared_state(indoc! {r"
3235        123 234 345
3236        456 5ˇ67 678
3237        "})
3238            .await;
3239        cx.simulate_shared_keystrokes("4 g e").await;
3240        cx.shared_state().await.assert_eq(indoc! {"
3241        12ˇ3 234 345
3242        456 567 678
3243        "});
3244
3245        // With punctuation
3246        cx.set_shared_state(indoc! {r"
3247        123 234 345
3248        4;5.6 5ˇ67 678
3249        789 890 901
3250        "})
3251            .await;
3252        cx.simulate_shared_keystrokes("g e").await;
3253        cx.shared_state().await.assert_eq(indoc! {"
3254          123 234 345
3255          4;5.ˇ6 567 678
3256          789 890 901
3257        "});
3258
3259        // With punctuation and count
3260        cx.set_shared_state(indoc! {r"
3261        123 234 345
3262        4;5.6 5ˇ67 678
3263        789 890 901
3264        "})
3265            .await;
3266        cx.simulate_shared_keystrokes("5 g e").await;
3267        cx.shared_state().await.assert_eq(indoc! {"
3268          123 234 345
3269          ˇ4;5.6 567 678
3270          789 890 901
3271        "});
3272
3273        // newlines
3274        cx.set_shared_state(indoc! {r"
3275        123 234 345
3276
3277        78ˇ9 890 901
3278        "})
3279            .await;
3280        cx.simulate_shared_keystrokes("g e").await;
3281        cx.shared_state().await.assert_eq(indoc! {"
3282          123 234 345
3283          ˇ
3284          789 890 901
3285        "});
3286        cx.simulate_shared_keystrokes("g e").await;
3287        cx.shared_state().await.assert_eq(indoc! {"
3288          123 234 34ˇ5
3289
3290          789 890 901
3291        "});
3292
3293        // With punctuation
3294        cx.set_shared_state(indoc! {r"
3295        123 234 345
3296        4;5.ˇ6 567 678
3297        789 890 901
3298        "})
3299            .await;
3300        cx.simulate_shared_keystrokes("g shift-e").await;
3301        cx.shared_state().await.assert_eq(indoc! {"
3302          123 234 34ˇ5
3303          4;5.6 567 678
3304          789 890 901
3305        "});
3306    }
3307
3308    #[gpui::test]
3309    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3310        let mut cx = NeovimBackedTestContext::new(cx).await;
3311
3312        cx.set_shared_state(indoc! {"
3313            fn aˇ() {
3314              return
3315            }
3316        "})
3317            .await;
3318        cx.simulate_shared_keystrokes("v $ %").await;
3319        cx.shared_state().await.assert_eq(indoc! {"
3320            fn a«() {
3321              return
3322            }ˇ»
3323        "});
3324    }
3325
3326    #[gpui::test]
3327    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3328        let mut cx = VimTestContext::new(cx, true).await;
3329
3330        cx.set_state(
3331            indoc! {"
3332            struct Foo {
3333            ˇ
3334            }
3335        "},
3336            Mode::Normal,
3337        );
3338
3339        cx.update_editor(|editor, cx| {
3340            let range = editor.selections.newest_anchor().range();
3341            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3342            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3343            editor.splice_inlays(vec![], vec![inlay], cx);
3344        });
3345
3346        cx.simulate_keystrokes("j");
3347        cx.assert_state(
3348            indoc! {"
3349            struct Foo {
3350
3351            ˇ}
3352        "},
3353            Mode::Normal,
3354        );
3355    }
3356}