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, ToPoint,
   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 => (
 851                start_of_document(map, point, maybe_times),
 852                SelectionGoal::None,
 853            ),
 854            EndOfDocument => (
 855                end_of_document(map, point, maybe_times),
 856                SelectionGoal::None,
 857            ),
 858            Matching => (matching(map, point), SelectionGoal::None),
 859            UnmatchedForward { char } => (
 860                unmatched_forward(map, point, *char, times),
 861                SelectionGoal::None,
 862            ),
 863            UnmatchedBackward { char } => (
 864                unmatched_backward(map, point, *char, times),
 865                SelectionGoal::None,
 866            ),
 867            // t f
 868            FindForward {
 869                before,
 870                char,
 871                mode,
 872                smartcase,
 873            } => {
 874                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
 875                    .map(|new_point| (new_point, SelectionGoal::None))
 876            }
 877            // T F
 878            FindBackward {
 879                after,
 880                char,
 881                mode,
 882                smartcase,
 883            } => (
 884                find_backward(map, point, *after, *char, times, *mode, *smartcase),
 885                SelectionGoal::None,
 886            ),
 887            Sneak {
 888                first_char,
 889                second_char,
 890                smartcase,
 891            } => {
 892                return sneak(map, point, *first_char, *second_char, times, *smartcase)
 893                    .map(|new_point| (new_point, SelectionGoal::None));
 894            }
 895            SneakBackward {
 896                first_char,
 897                second_char,
 898                smartcase,
 899            } => {
 900                return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
 901                    .map(|new_point| (new_point, SelectionGoal::None));
 902            }
 903            // ; -- repeat the last find done with t, f, T, F
 904            RepeatFind { last_find } => match **last_find {
 905                Motion::FindForward {
 906                    before,
 907                    char,
 908                    mode,
 909                    smartcase,
 910                } => {
 911                    let mut new_point =
 912                        find_forward(map, point, before, char, times, mode, smartcase);
 913                    if new_point == Some(point) {
 914                        new_point =
 915                            find_forward(map, point, before, char, times + 1, mode, smartcase);
 916                    }
 917
 918                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 919                }
 920
 921                Motion::FindBackward {
 922                    after,
 923                    char,
 924                    mode,
 925                    smartcase,
 926                } => {
 927                    let mut new_point =
 928                        find_backward(map, point, after, char, times, mode, smartcase);
 929                    if new_point == point {
 930                        new_point =
 931                            find_backward(map, point, after, char, times + 1, mode, smartcase);
 932                    }
 933
 934                    (new_point, SelectionGoal::None)
 935                }
 936                Motion::Sneak {
 937                    first_char,
 938                    second_char,
 939                    smartcase,
 940                } => {
 941                    let mut new_point =
 942                        sneak(map, point, first_char, second_char, times, smartcase);
 943                    if new_point == Some(point) {
 944                        new_point =
 945                            sneak(map, point, first_char, second_char, times + 1, smartcase);
 946                    }
 947
 948                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 949                }
 950
 951                Motion::SneakBackward {
 952                    first_char,
 953                    second_char,
 954                    smartcase,
 955                } => {
 956                    let mut new_point =
 957                        sneak_backward(map, point, first_char, second_char, times, smartcase);
 958                    if new_point == Some(point) {
 959                        new_point = sneak_backward(
 960                            map,
 961                            point,
 962                            first_char,
 963                            second_char,
 964                            times + 1,
 965                            smartcase,
 966                        );
 967                    }
 968
 969                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 970                }
 971                _ => return None,
 972            },
 973            // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
 974            RepeatFindReversed { last_find } => match **last_find {
 975                Motion::FindForward {
 976                    before,
 977                    char,
 978                    mode,
 979                    smartcase,
 980                } => {
 981                    let mut new_point =
 982                        find_backward(map, point, before, char, times, mode, smartcase);
 983                    if new_point == point {
 984                        new_point =
 985                            find_backward(map, point, before, char, times + 1, mode, smartcase);
 986                    }
 987
 988                    (new_point, SelectionGoal::None)
 989                }
 990
 991                Motion::FindBackward {
 992                    after,
 993                    char,
 994                    mode,
 995                    smartcase,
 996                } => {
 997                    let mut new_point =
 998                        find_forward(map, point, after, char, times, mode, smartcase);
 999                    if new_point == Some(point) {
1000                        new_point =
1001                            find_forward(map, point, after, char, times + 1, mode, smartcase);
1002                    }
1003
1004                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1005                }
1006
1007                Motion::Sneak {
1008                    first_char,
1009                    second_char,
1010                    smartcase,
1011                } => {
1012                    let mut new_point =
1013                        sneak_backward(map, point, first_char, second_char, times, smartcase);
1014                    if new_point == Some(point) {
1015                        new_point = sneak_backward(
1016                            map,
1017                            point,
1018                            first_char,
1019                            second_char,
1020                            times + 1,
1021                            smartcase,
1022                        );
1023                    }
1024
1025                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1026                }
1027
1028                Motion::SneakBackward {
1029                    first_char,
1030                    second_char,
1031                    smartcase,
1032                } => {
1033                    let mut new_point =
1034                        sneak(map, point, first_char, second_char, times, smartcase);
1035                    if new_point == Some(point) {
1036                        new_point =
1037                            sneak(map, point, first_char, second_char, times + 1, smartcase);
1038                    }
1039
1040                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1041                }
1042                _ => return None,
1043            },
1044            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
1045            PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
1046            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
1047            EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
1048            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
1049            WindowTop => window_top(map, point, text_layout_details, times - 1),
1050            WindowMiddle => window_middle(map, point, text_layout_details),
1051            WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
1052            Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
1053            ZedSearchResult { new_selections, .. } => {
1054                // There will be only one selection, as
1055                // Search::SelectNextMatch selects a single match.
1056                if let Some(new_selection) = new_selections.first() {
1057                    (
1058                        new_selection.start.to_display_point(map),
1059                        SelectionGoal::None,
1060                    )
1061                } else {
1062                    return None;
1063                }
1064            }
1065            NextSectionStart => (
1066                section_motion(map, point, times, Direction::Next, true),
1067                SelectionGoal::None,
1068            ),
1069            NextSectionEnd => (
1070                section_motion(map, point, times, Direction::Next, false),
1071                SelectionGoal::None,
1072            ),
1073            PreviousSectionStart => (
1074                section_motion(map, point, times, Direction::Prev, true),
1075                SelectionGoal::None,
1076            ),
1077            PreviousSectionEnd => (
1078                section_motion(map, point, times, Direction::Prev, false),
1079                SelectionGoal::None,
1080            ),
1081
1082            NextMethodStart => (
1083                method_motion(map, point, times, Direction::Next, true),
1084                SelectionGoal::None,
1085            ),
1086            NextMethodEnd => (
1087                method_motion(map, point, times, Direction::Next, false),
1088                SelectionGoal::None,
1089            ),
1090            PreviousMethodStart => (
1091                method_motion(map, point, times, Direction::Prev, true),
1092                SelectionGoal::None,
1093            ),
1094            PreviousMethodEnd => (
1095                method_motion(map, point, times, Direction::Prev, false),
1096                SelectionGoal::None,
1097            ),
1098            NextComment => (
1099                comment_motion(map, point, times, Direction::Next),
1100                SelectionGoal::None,
1101            ),
1102            PreviousComment => (
1103                comment_motion(map, point, times, Direction::Prev),
1104                SelectionGoal::None,
1105            ),
1106        };
1107
1108        (new_point != point || infallible).then_some((new_point, goal))
1109    }
1110
1111    // Get the range value after self is applied to the specified selection.
1112    pub fn range(
1113        &self,
1114        map: &DisplaySnapshot,
1115        selection: Selection<DisplayPoint>,
1116        times: Option<usize>,
1117        expand_to_surrounding_newline: bool,
1118        text_layout_details: &TextLayoutDetails,
1119    ) -> Option<Range<DisplayPoint>> {
1120        if let Motion::ZedSearchResult {
1121            prior_selections,
1122            new_selections,
1123        } = self
1124        {
1125            if let Some((prior_selection, new_selection)) =
1126                prior_selections.first().zip(new_selections.first())
1127            {
1128                let start = prior_selection
1129                    .start
1130                    .to_display_point(map)
1131                    .min(new_selection.start.to_display_point(map));
1132                let end = new_selection
1133                    .end
1134                    .to_display_point(map)
1135                    .max(prior_selection.end.to_display_point(map));
1136
1137                if start < end {
1138                    return Some(start..end);
1139                } else {
1140                    return Some(end..start);
1141                }
1142            } else {
1143                return None;
1144            }
1145        }
1146
1147        if let Some((new_head, goal)) = self.move_point(
1148            map,
1149            selection.head(),
1150            selection.goal,
1151            times,
1152            text_layout_details,
1153        ) {
1154            let mut selection = selection.clone();
1155            selection.set_head(new_head, goal);
1156
1157            if self.linewise() {
1158                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1159
1160                if expand_to_surrounding_newline {
1161                    if selection.end.row() < map.max_point().row() {
1162                        *selection.end.row_mut() += 1;
1163                        *selection.end.column_mut() = 0;
1164                        selection.end = map.clip_point(selection.end, Bias::Right);
1165                        // Don't reset the end here
1166                        return Some(selection.start..selection.end);
1167                    } else if selection.start.row().0 > 0 {
1168                        *selection.start.row_mut() -= 1;
1169                        *selection.start.column_mut() = map.line_len(selection.start.row());
1170                        selection.start = map.clip_point(selection.start, Bias::Left);
1171                    }
1172                }
1173
1174                selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1175            } else {
1176                // Another special case: When using the "w" motion in combination with an
1177                // operator and the last word moved over is at the end of a line, the end of
1178                // that word becomes the end of the operated text, not the first word in the
1179                // next line.
1180                if let Motion::NextWordStart {
1181                    ignore_punctuation: _,
1182                } = self
1183                {
1184                    let start_row = MultiBufferRow(selection.start.to_point(map).row);
1185                    if selection.end.to_point(map).row > start_row.0 {
1186                        selection.end =
1187                            Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
1188                                .to_display_point(map)
1189                    }
1190                }
1191
1192                // If the motion is exclusive and the end of the motion is in column 1, the
1193                // end of the motion is moved to the end of the previous line and the motion
1194                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1195                // but "d}" will not include that line.
1196                let mut inclusive = self.inclusive();
1197                let start_point = selection.start.to_point(map);
1198                let mut end_point = selection.end.to_point(map);
1199
1200                // DisplayPoint
1201
1202                if !inclusive
1203                    && self != &Motion::Backspace
1204                    && end_point.row > start_point.row
1205                    && end_point.column == 0
1206                {
1207                    inclusive = true;
1208                    end_point.row -= 1;
1209                    end_point.column = 0;
1210                    selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1211                }
1212
1213                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
1214                    selection.end = movement::saturating_right(map, selection.end)
1215                }
1216            }
1217            Some(selection.start..selection.end)
1218        } else {
1219            None
1220        }
1221    }
1222
1223    // Expands a selection using self for an operator
1224    pub fn expand_selection(
1225        &self,
1226        map: &DisplaySnapshot,
1227        selection: &mut Selection<DisplayPoint>,
1228        times: Option<usize>,
1229        expand_to_surrounding_newline: bool,
1230        text_layout_details: &TextLayoutDetails,
1231    ) -> bool {
1232        if let Some(range) = self.range(
1233            map,
1234            selection.clone(),
1235            times,
1236            expand_to_surrounding_newline,
1237            text_layout_details,
1238        ) {
1239            selection.start = range.start;
1240            selection.end = range.end;
1241            true
1242        } else {
1243            false
1244        }
1245    }
1246}
1247
1248fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1249    for _ in 0..times {
1250        point = movement::saturating_left(map, point);
1251        if point.column() == 0 {
1252            break;
1253        }
1254    }
1255    point
1256}
1257
1258pub(crate) fn backspace(
1259    map: &DisplaySnapshot,
1260    mut point: DisplayPoint,
1261    times: usize,
1262) -> DisplayPoint {
1263    for _ in 0..times {
1264        point = movement::left(map, point);
1265        if point.is_zero() {
1266            break;
1267        }
1268    }
1269    point
1270}
1271
1272fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1273    for _ in 0..times {
1274        point = wrapping_right(map, point);
1275        if point == map.max_point() {
1276            break;
1277        }
1278    }
1279    point
1280}
1281
1282fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
1283    let max_column = map.line_len(point.row()).saturating_sub(1);
1284    if point.column() < max_column {
1285        *point.column_mut() += 1;
1286    } else if point.row() < map.max_point().row() {
1287        *point.row_mut() += 1;
1288        *point.column_mut() = 0;
1289    }
1290    point
1291}
1292
1293pub(crate) fn start_of_relative_buffer_row(
1294    map: &DisplaySnapshot,
1295    point: DisplayPoint,
1296    times: isize,
1297) -> DisplayPoint {
1298    let start = map.display_point_to_fold_point(point, Bias::Left);
1299    let target = start.row() as isize + times;
1300    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1301
1302    map.clip_point(
1303        map.fold_point_to_display_point(
1304            map.fold_snapshot
1305                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1306        ),
1307        Bias::Right,
1308    )
1309}
1310
1311fn up_down_buffer_rows(
1312    map: &DisplaySnapshot,
1313    point: DisplayPoint,
1314    mut goal: SelectionGoal,
1315    times: isize,
1316    text_layout_details: &TextLayoutDetails,
1317) -> (DisplayPoint, SelectionGoal) {
1318    let bias = if times < 0 { Bias::Left } else { Bias::Right };
1319    let start = map.display_point_to_fold_point(point, Bias::Left);
1320    let begin_folded_line = map.fold_point_to_display_point(
1321        map.fold_snapshot
1322            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1323    );
1324    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1325
1326    let (goal_wrap, goal_x) = match goal {
1327        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1328        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1329        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1330        _ => {
1331            let x = map.x_for_display_point(point, text_layout_details);
1332            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1333            (select_nth_wrapped_row, x.0)
1334        }
1335    };
1336
1337    let target = start.row() as isize + times;
1338    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1339
1340    let mut begin_folded_line = map.fold_point_to_display_point(
1341        map.fold_snapshot
1342            .clip_point(FoldPoint::new(new_row, 0), bias),
1343    );
1344
1345    let mut i = 0;
1346    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1347        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1348        if map
1349            .display_point_to_fold_point(next_folded_line, bias)
1350            .row()
1351            == new_row
1352        {
1353            i += 1;
1354            begin_folded_line = next_folded_line;
1355        } else {
1356            break;
1357        }
1358    }
1359
1360    let new_col = if i == goal_wrap {
1361        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1362    } else {
1363        map.line_len(begin_folded_line.row())
1364    };
1365
1366    (
1367        map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
1368        goal,
1369    )
1370}
1371
1372fn down_display(
1373    map: &DisplaySnapshot,
1374    mut point: DisplayPoint,
1375    mut goal: SelectionGoal,
1376    times: usize,
1377    text_layout_details: &TextLayoutDetails,
1378) -> (DisplayPoint, SelectionGoal) {
1379    for _ in 0..times {
1380        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1381    }
1382
1383    (point, goal)
1384}
1385
1386fn up_display(
1387    map: &DisplaySnapshot,
1388    mut point: DisplayPoint,
1389    mut goal: SelectionGoal,
1390    times: usize,
1391    text_layout_details: &TextLayoutDetails,
1392) -> (DisplayPoint, SelectionGoal) {
1393    for _ in 0..times {
1394        (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1395    }
1396
1397    (point, goal)
1398}
1399
1400pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1401    for _ in 0..times {
1402        let new_point = movement::saturating_right(map, point);
1403        if point == new_point {
1404            break;
1405        }
1406        point = new_point;
1407    }
1408    point
1409}
1410
1411pub(crate) fn next_char(
1412    map: &DisplaySnapshot,
1413    point: DisplayPoint,
1414    allow_cross_newline: bool,
1415) -> DisplayPoint {
1416    let mut new_point = point;
1417    let mut max_column = map.line_len(new_point.row());
1418    if !allow_cross_newline {
1419        max_column -= 1;
1420    }
1421    if new_point.column() < max_column {
1422        *new_point.column_mut() += 1;
1423    } else if new_point < map.max_point() && allow_cross_newline {
1424        *new_point.row_mut() += 1;
1425        *new_point.column_mut() = 0;
1426    }
1427    map.clip_ignoring_line_ends(new_point, Bias::Right)
1428}
1429
1430pub(crate) fn next_word_start(
1431    map: &DisplaySnapshot,
1432    mut point: DisplayPoint,
1433    ignore_punctuation: bool,
1434    times: usize,
1435) -> DisplayPoint {
1436    let classifier = map
1437        .buffer_snapshot
1438        .char_classifier_at(point.to_point(map))
1439        .ignore_punctuation(ignore_punctuation);
1440    for _ in 0..times {
1441        let mut crossed_newline = false;
1442        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1443            let left_kind = classifier.kind(left);
1444            let right_kind = classifier.kind(right);
1445            let at_newline = right == '\n';
1446
1447            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1448                || at_newline && crossed_newline
1449                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1450
1451            crossed_newline |= at_newline;
1452            found
1453        });
1454        if point == new_point {
1455            break;
1456        }
1457        point = new_point;
1458    }
1459    point
1460}
1461
1462pub(crate) fn next_word_end(
1463    map: &DisplaySnapshot,
1464    mut point: DisplayPoint,
1465    ignore_punctuation: bool,
1466    times: usize,
1467    allow_cross_newline: bool,
1468) -> DisplayPoint {
1469    let classifier = map
1470        .buffer_snapshot
1471        .char_classifier_at(point.to_point(map))
1472        .ignore_punctuation(ignore_punctuation);
1473    for _ in 0..times {
1474        let new_point = next_char(map, point, allow_cross_newline);
1475        let mut need_next_char = false;
1476        let new_point = movement::find_boundary_exclusive(
1477            map,
1478            new_point,
1479            FindRange::MultiLine,
1480            |left, right| {
1481                let left_kind = classifier.kind(left);
1482                let right_kind = classifier.kind(right);
1483                let at_newline = right == '\n';
1484
1485                if !allow_cross_newline && at_newline {
1486                    need_next_char = true;
1487                    return true;
1488                }
1489
1490                left_kind != right_kind && left_kind != CharKind::Whitespace
1491            },
1492        );
1493        let new_point = if need_next_char {
1494            next_char(map, new_point, true)
1495        } else {
1496            new_point
1497        };
1498        let new_point = map.clip_point(new_point, Bias::Left);
1499        if point == new_point {
1500            break;
1501        }
1502        point = new_point;
1503    }
1504    point
1505}
1506
1507fn previous_word_start(
1508    map: &DisplaySnapshot,
1509    mut point: DisplayPoint,
1510    ignore_punctuation: bool,
1511    times: usize,
1512) -> DisplayPoint {
1513    let classifier = map
1514        .buffer_snapshot
1515        .char_classifier_at(point.to_point(map))
1516        .ignore_punctuation(ignore_punctuation);
1517    for _ in 0..times {
1518        // This works even though find_preceding_boundary is called for every character in the line containing
1519        // cursor because the newline is checked only once.
1520        let new_point = movement::find_preceding_boundary_display_point(
1521            map,
1522            point,
1523            FindRange::MultiLine,
1524            |left, right| {
1525                let left_kind = classifier.kind(left);
1526                let right_kind = classifier.kind(right);
1527
1528                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1529            },
1530        );
1531        if point == new_point {
1532            break;
1533        }
1534        point = new_point;
1535    }
1536    point
1537}
1538
1539fn previous_word_end(
1540    map: &DisplaySnapshot,
1541    point: DisplayPoint,
1542    ignore_punctuation: bool,
1543    times: usize,
1544) -> DisplayPoint {
1545    let classifier = map
1546        .buffer_snapshot
1547        .char_classifier_at(point.to_point(map))
1548        .ignore_punctuation(ignore_punctuation);
1549    let mut point = point.to_point(map);
1550
1551    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1552        point.column += 1;
1553    }
1554    for _ in 0..times {
1555        let new_point = movement::find_preceding_boundary_point(
1556            &map.buffer_snapshot,
1557            point,
1558            FindRange::MultiLine,
1559            |left, right| {
1560                let left_kind = classifier.kind(left);
1561                let right_kind = classifier.kind(right);
1562                match (left_kind, right_kind) {
1563                    (CharKind::Punctuation, CharKind::Whitespace)
1564                    | (CharKind::Punctuation, CharKind::Word)
1565                    | (CharKind::Word, CharKind::Whitespace)
1566                    | (CharKind::Word, CharKind::Punctuation) => true,
1567                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1568                    _ => false,
1569                }
1570            },
1571        );
1572        if new_point == point {
1573            break;
1574        }
1575        point = new_point;
1576    }
1577    movement::saturating_left(map, point.to_display_point(map))
1578}
1579
1580fn next_subword_start(
1581    map: &DisplaySnapshot,
1582    mut point: DisplayPoint,
1583    ignore_punctuation: bool,
1584    times: usize,
1585) -> DisplayPoint {
1586    let classifier = map
1587        .buffer_snapshot
1588        .char_classifier_at(point.to_point(map))
1589        .ignore_punctuation(ignore_punctuation);
1590    for _ in 0..times {
1591        let mut crossed_newline = false;
1592        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1593            let left_kind = classifier.kind(left);
1594            let right_kind = classifier.kind(right);
1595            let at_newline = right == '\n';
1596
1597            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1598            let is_subword_start =
1599                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1600
1601            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1602                || at_newline && crossed_newline
1603                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1604
1605            crossed_newline |= at_newline;
1606            found
1607        });
1608        if point == new_point {
1609            break;
1610        }
1611        point = new_point;
1612    }
1613    point
1614}
1615
1616pub(crate) fn next_subword_end(
1617    map: &DisplaySnapshot,
1618    mut point: DisplayPoint,
1619    ignore_punctuation: bool,
1620    times: usize,
1621    allow_cross_newline: bool,
1622) -> DisplayPoint {
1623    let classifier = map
1624        .buffer_snapshot
1625        .char_classifier_at(point.to_point(map))
1626        .ignore_punctuation(ignore_punctuation);
1627    for _ in 0..times {
1628        let new_point = next_char(map, point, allow_cross_newline);
1629
1630        let mut crossed_newline = false;
1631        let mut need_backtrack = false;
1632        let new_point =
1633            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1634                let left_kind = classifier.kind(left);
1635                let right_kind = classifier.kind(right);
1636                let at_newline = right == '\n';
1637
1638                if !allow_cross_newline && at_newline {
1639                    return true;
1640                }
1641
1642                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1643                let is_subword_end =
1644                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1645
1646                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1647
1648                if found && (is_word_end || is_subword_end) {
1649                    need_backtrack = true;
1650                }
1651
1652                crossed_newline |= at_newline;
1653                found
1654            });
1655        let mut new_point = map.clip_point(new_point, Bias::Left);
1656        if need_backtrack {
1657            *new_point.column_mut() -= 1;
1658        }
1659        if point == new_point {
1660            break;
1661        }
1662        point = new_point;
1663    }
1664    point
1665}
1666
1667fn previous_subword_start(
1668    map: &DisplaySnapshot,
1669    mut point: DisplayPoint,
1670    ignore_punctuation: bool,
1671    times: usize,
1672) -> DisplayPoint {
1673    let classifier = map
1674        .buffer_snapshot
1675        .char_classifier_at(point.to_point(map))
1676        .ignore_punctuation(ignore_punctuation);
1677    for _ in 0..times {
1678        let mut crossed_newline = false;
1679        // This works even though find_preceding_boundary is called for every character in the line containing
1680        // cursor because the newline is checked only once.
1681        let new_point = movement::find_preceding_boundary_display_point(
1682            map,
1683            point,
1684            FindRange::MultiLine,
1685            |left, right| {
1686                let left_kind = classifier.kind(left);
1687                let right_kind = classifier.kind(right);
1688                let at_newline = right == '\n';
1689
1690                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1691                let is_subword_start =
1692                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1693
1694                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1695                    || at_newline && crossed_newline
1696                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1697
1698                crossed_newline |= at_newline;
1699
1700                found
1701            },
1702        );
1703        if point == new_point {
1704            break;
1705        }
1706        point = new_point;
1707    }
1708    point
1709}
1710
1711fn previous_subword_end(
1712    map: &DisplaySnapshot,
1713    point: DisplayPoint,
1714    ignore_punctuation: bool,
1715    times: usize,
1716) -> DisplayPoint {
1717    let classifier = map
1718        .buffer_snapshot
1719        .char_classifier_at(point.to_point(map))
1720        .ignore_punctuation(ignore_punctuation);
1721    let mut point = point.to_point(map);
1722
1723    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1724        point.column += 1;
1725    }
1726    for _ in 0..times {
1727        let new_point = movement::find_preceding_boundary_point(
1728            &map.buffer_snapshot,
1729            point,
1730            FindRange::MultiLine,
1731            |left, right| {
1732                let left_kind = classifier.kind(left);
1733                let right_kind = classifier.kind(right);
1734
1735                let is_subword_end =
1736                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1737
1738                if is_subword_end {
1739                    return true;
1740                }
1741
1742                match (left_kind, right_kind) {
1743                    (CharKind::Word, CharKind::Whitespace)
1744                    | (CharKind::Word, CharKind::Punctuation) => true,
1745                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1746                    _ => false,
1747                }
1748            },
1749        );
1750        if new_point == point {
1751            break;
1752        }
1753        point = new_point;
1754    }
1755    movement::saturating_left(map, point.to_display_point(map))
1756}
1757
1758pub(crate) fn first_non_whitespace(
1759    map: &DisplaySnapshot,
1760    display_lines: bool,
1761    from: DisplayPoint,
1762) -> DisplayPoint {
1763    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1764    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1765    for (ch, offset) in map.buffer_chars_at(start_offset) {
1766        if ch == '\n' {
1767            return from;
1768        }
1769
1770        start_offset = offset;
1771
1772        if classifier.kind(ch) != CharKind::Whitespace {
1773            break;
1774        }
1775    }
1776
1777    start_offset.to_display_point(map)
1778}
1779
1780pub(crate) fn last_non_whitespace(
1781    map: &DisplaySnapshot,
1782    from: DisplayPoint,
1783    count: usize,
1784) -> DisplayPoint {
1785    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1786    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1787
1788    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1789    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1790        if classifier.kind(ch) != CharKind::Whitespace {
1791            return end_of_line.to_display_point(map);
1792        }
1793    }
1794
1795    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1796        if ch == '\n' {
1797            break;
1798        }
1799        end_of_line = offset;
1800        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1801            break;
1802        }
1803    }
1804
1805    end_of_line.to_display_point(map)
1806}
1807
1808pub(crate) fn start_of_line(
1809    map: &DisplaySnapshot,
1810    display_lines: bool,
1811    point: DisplayPoint,
1812) -> DisplayPoint {
1813    if display_lines {
1814        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1815    } else {
1816        map.prev_line_boundary(point.to_point(map)).1
1817    }
1818}
1819
1820pub(crate) fn end_of_line(
1821    map: &DisplaySnapshot,
1822    display_lines: bool,
1823    mut point: DisplayPoint,
1824    times: usize,
1825) -> DisplayPoint {
1826    if times > 1 {
1827        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1828    }
1829    if display_lines {
1830        map.clip_point(
1831            DisplayPoint::new(point.row(), map.line_len(point.row())),
1832            Bias::Left,
1833        )
1834    } else {
1835        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1836    }
1837}
1838
1839fn sentence_backwards(
1840    map: &DisplaySnapshot,
1841    point: DisplayPoint,
1842    mut times: usize,
1843) -> DisplayPoint {
1844    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1845    let mut chars = map.reverse_buffer_chars_at(start).peekable();
1846
1847    let mut was_newline = map
1848        .buffer_chars_at(start)
1849        .next()
1850        .is_some_and(|(c, _)| c == '\n');
1851
1852    while let Some((ch, offset)) = chars.next() {
1853        let start_of_next_sentence = if was_newline && ch == '\n' {
1854            Some(offset + ch.len_utf8())
1855        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1856            Some(next_non_blank(map, offset + ch.len_utf8()))
1857        } else if ch == '.' || ch == '?' || ch == '!' {
1858            start_of_next_sentence(map, offset + ch.len_utf8())
1859        } else {
1860            None
1861        };
1862
1863        if let Some(start_of_next_sentence) = start_of_next_sentence {
1864            if start_of_next_sentence < start {
1865                times = times.saturating_sub(1);
1866            }
1867            if times == 0 || offset == 0 {
1868                return map.clip_point(
1869                    start_of_next_sentence
1870                        .to_offset(&map.buffer_snapshot)
1871                        .to_display_point(map),
1872                    Bias::Left,
1873                );
1874            }
1875        }
1876        if was_newline {
1877            start = offset;
1878        }
1879        was_newline = ch == '\n';
1880    }
1881
1882    DisplayPoint::zero()
1883}
1884
1885fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1886    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1887    let mut chars = map.buffer_chars_at(start).peekable();
1888
1889    let mut was_newline = map
1890        .reverse_buffer_chars_at(start)
1891        .next()
1892        .is_some_and(|(c, _)| c == '\n')
1893        && chars.peek().is_some_and(|(c, _)| *c == '\n');
1894
1895    while let Some((ch, offset)) = chars.next() {
1896        if was_newline && ch == '\n' {
1897            continue;
1898        }
1899        let start_of_next_sentence = if was_newline {
1900            Some(next_non_blank(map, offset))
1901        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1902            Some(next_non_blank(map, offset + ch.len_utf8()))
1903        } else if ch == '.' || ch == '?' || ch == '!' {
1904            start_of_next_sentence(map, offset + ch.len_utf8())
1905        } else {
1906            None
1907        };
1908
1909        if let Some(start_of_next_sentence) = start_of_next_sentence {
1910            times = times.saturating_sub(1);
1911            if times == 0 {
1912                return map.clip_point(
1913                    start_of_next_sentence
1914                        .to_offset(&map.buffer_snapshot)
1915                        .to_display_point(map),
1916                    Bias::Right,
1917                );
1918            }
1919        }
1920
1921        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1922    }
1923
1924    map.max_point()
1925}
1926
1927fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1928    for (c, o) in map.buffer_chars_at(start) {
1929        if c == '\n' || !c.is_whitespace() {
1930            return o;
1931        }
1932    }
1933
1934    map.buffer_snapshot.len()
1935}
1936
1937// given the offset after a ., !, or ? find the start of the next sentence.
1938// if this is not a sentence boundary, returns None.
1939fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1940    let chars = map.buffer_chars_at(end_of_sentence);
1941    let mut seen_space = false;
1942
1943    for (char, offset) in chars {
1944        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1945            continue;
1946        }
1947
1948        if char == '\n' && seen_space {
1949            return Some(offset);
1950        } else if char.is_whitespace() {
1951            seen_space = true;
1952        } else if seen_space {
1953            return Some(offset);
1954        } else {
1955            return None;
1956        }
1957    }
1958
1959    Some(map.buffer_snapshot.len())
1960}
1961
1962fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
1963    let point = map.display_point_to_point(display_point, Bias::Left);
1964    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
1965        return display_point;
1966    };
1967    let offset = excerpt.buffer().point_to_offset(
1968        excerpt
1969            .buffer()
1970            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
1971    );
1972    let buffer_range = excerpt.buffer_range();
1973    if offset >= buffer_range.start && offset <= buffer_range.end {
1974        let point = map
1975            .buffer_snapshot
1976            .offset_to_point(excerpt.map_offset_from_buffer(offset));
1977        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
1978    }
1979    let mut last_position = None;
1980    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
1981        let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
1982            ..language::ToOffset::to_offset(&range.context.end, &buffer);
1983        if offset >= excerpt_range.start && offset <= excerpt_range.end {
1984            let text_anchor = buffer.anchor_after(offset);
1985            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
1986            return anchor.to_display_point(map);
1987        } else if offset <= excerpt_range.start {
1988            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
1989            return anchor.to_display_point(map);
1990        } else {
1991            last_position = Some(Anchor::in_buffer(
1992                excerpt,
1993                buffer.remote_id(),
1994                range.context.end,
1995            ));
1996        }
1997    }
1998
1999    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2000    last_point.column = point.column;
2001
2002    map.clip_point(
2003        map.point_to_display_point(
2004            map.buffer_snapshot.clip_point(point, Bias::Left),
2005            Bias::Left,
2006        ),
2007        Bias::Left,
2008    )
2009}
2010
2011fn start_of_document(
2012    map: &DisplaySnapshot,
2013    display_point: DisplayPoint,
2014    maybe_times: Option<usize>,
2015) -> DisplayPoint {
2016    if let Some(times) = maybe_times {
2017        return go_to_line(map, display_point, times);
2018    }
2019
2020    let point = map.display_point_to_point(display_point, Bias::Left);
2021    let mut first_point = Point::zero();
2022    first_point.column = point.column;
2023
2024    map.clip_point(
2025        map.point_to_display_point(
2026            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2027            Bias::Left,
2028        ),
2029        Bias::Left,
2030    )
2031}
2032
2033fn end_of_document(
2034    map: &DisplaySnapshot,
2035    display_point: DisplayPoint,
2036    maybe_times: Option<usize>,
2037) -> DisplayPoint {
2038    if let Some(times) = maybe_times {
2039        return go_to_line(map, display_point, times);
2040    };
2041    let point = map.display_point_to_point(display_point, Bias::Left);
2042    let mut last_point = map.buffer_snapshot.max_point();
2043    last_point.column = point.column;
2044
2045    map.clip_point(
2046        map.point_to_display_point(
2047            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2048            Bias::Left,
2049        ),
2050        Bias::Left,
2051    )
2052}
2053
2054fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2055    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2056    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2057
2058    if head > outer.start && head < inner.start {
2059        let mut offset = inner.end.to_offset(map, Bias::Left);
2060        for c in map.buffer_snapshot.chars_at(offset) {
2061            if c == '/' || c == '\n' || c == '>' {
2062                return Some(offset.to_display_point(map));
2063            }
2064            offset += c.len_utf8();
2065        }
2066    } else {
2067        let mut offset = outer.start.to_offset(map, Bias::Left);
2068        for c in map.buffer_snapshot.chars_at(offset) {
2069            offset += c.len_utf8();
2070            if c == '<' || c == '\n' {
2071                return Some(offset.to_display_point(map));
2072            }
2073        }
2074    }
2075
2076    return None;
2077}
2078
2079fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2080    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2081    let display_point = map.clip_at_line_end(display_point);
2082    let point = display_point.to_point(map);
2083    let offset = point.to_offset(&map.buffer_snapshot);
2084
2085    // Ensure the range is contained by the current line.
2086    let mut line_end = map.next_line_boundary(point).0;
2087    if line_end == point {
2088        line_end = map.max_point().to_point(map);
2089    }
2090
2091    let line_range = map.prev_line_boundary(point).0..line_end;
2092    let visible_line_range =
2093        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2094    let ranges = map
2095        .buffer_snapshot
2096        .bracket_ranges(visible_line_range.clone());
2097    if let Some(ranges) = ranges {
2098        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2099            ..line_range.end.to_offset(&map.buffer_snapshot);
2100        let mut closest_pair_destination = None;
2101        let mut closest_distance = usize::MAX;
2102
2103        for (open_range, close_range) in ranges {
2104            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2105                if offset > open_range.start && offset < close_range.start {
2106                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2107                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2108                        return display_point;
2109                    }
2110                    if let Some(tag) = matching_tag(map, display_point) {
2111                        return tag;
2112                    }
2113                } else if close_range.contains(&offset) {
2114                    return open_range.start.to_display_point(map);
2115                } else if open_range.contains(&offset) {
2116                    return (close_range.end - 1).to_display_point(map);
2117                }
2118            }
2119
2120            if (open_range.contains(&offset) || open_range.start >= offset)
2121                && line_range.contains(&open_range.start)
2122            {
2123                let distance = open_range.start.saturating_sub(offset);
2124                if distance < closest_distance {
2125                    closest_pair_destination = Some(close_range.start);
2126                    closest_distance = distance;
2127                    continue;
2128                }
2129            }
2130
2131            if (close_range.contains(&offset) || close_range.start >= offset)
2132                && line_range.contains(&close_range.start)
2133            {
2134                let distance = close_range.start.saturating_sub(offset);
2135                if distance < closest_distance {
2136                    closest_pair_destination = Some(open_range.start);
2137                    closest_distance = distance;
2138                    continue;
2139                }
2140            }
2141
2142            continue;
2143        }
2144
2145        closest_pair_destination
2146            .map(|destination| destination.to_display_point(map))
2147            .unwrap_or(display_point)
2148    } else {
2149        display_point
2150    }
2151}
2152
2153fn unmatched_forward(
2154    map: &DisplaySnapshot,
2155    mut display_point: DisplayPoint,
2156    char: char,
2157    times: usize,
2158) -> DisplayPoint {
2159    for _ in 0..times {
2160        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2161        let point = display_point.to_point(map);
2162        let offset = point.to_offset(&map.buffer_snapshot);
2163
2164        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2165        let Some(ranges) = ranges else { break };
2166        let mut closest_closing_destination = None;
2167        let mut closest_distance = usize::MAX;
2168
2169        for (_, close_range) in ranges {
2170            if close_range.start > offset {
2171                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2172                if Some(char) == chars.next() {
2173                    let distance = close_range.start - offset;
2174                    if distance < closest_distance {
2175                        closest_closing_destination = Some(close_range.start);
2176                        closest_distance = distance;
2177                        continue;
2178                    }
2179                }
2180            }
2181        }
2182
2183        let new_point = closest_closing_destination
2184            .map(|destination| destination.to_display_point(map))
2185            .unwrap_or(display_point);
2186        if new_point == display_point {
2187            break;
2188        }
2189        display_point = new_point;
2190    }
2191    return display_point;
2192}
2193
2194fn unmatched_backward(
2195    map: &DisplaySnapshot,
2196    mut display_point: DisplayPoint,
2197    char: char,
2198    times: usize,
2199) -> DisplayPoint {
2200    for _ in 0..times {
2201        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2202        let point = display_point.to_point(map);
2203        let offset = point.to_offset(&map.buffer_snapshot);
2204
2205        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2206        let Some(ranges) = ranges else {
2207            break;
2208        };
2209
2210        let mut closest_starting_destination = None;
2211        let mut closest_distance = usize::MAX;
2212
2213        for (start_range, _) in ranges {
2214            if start_range.start < offset {
2215                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2216                if Some(char) == chars.next() {
2217                    let distance = offset - start_range.start;
2218                    if distance < closest_distance {
2219                        closest_starting_destination = Some(start_range.start);
2220                        closest_distance = distance;
2221                        continue;
2222                    }
2223                }
2224            }
2225        }
2226
2227        let new_point = closest_starting_destination
2228            .map(|destination| destination.to_display_point(map))
2229            .unwrap_or(display_point);
2230        if new_point == display_point {
2231            break;
2232        } else {
2233            display_point = new_point;
2234        }
2235    }
2236    display_point
2237}
2238
2239fn find_forward(
2240    map: &DisplaySnapshot,
2241    from: DisplayPoint,
2242    before: bool,
2243    target: char,
2244    times: usize,
2245    mode: FindRange,
2246    smartcase: bool,
2247) -> Option<DisplayPoint> {
2248    let mut to = from;
2249    let mut found = false;
2250
2251    for _ in 0..times {
2252        found = false;
2253        let new_to = find_boundary(map, to, mode, |_, right| {
2254            found = is_character_match(target, right, smartcase);
2255            found
2256        });
2257        if to == new_to {
2258            break;
2259        }
2260        to = new_to;
2261    }
2262
2263    if found {
2264        if before && to.column() > 0 {
2265            *to.column_mut() -= 1;
2266            Some(map.clip_point(to, Bias::Left))
2267        } else {
2268            Some(to)
2269        }
2270    } else {
2271        None
2272    }
2273}
2274
2275fn find_backward(
2276    map: &DisplaySnapshot,
2277    from: DisplayPoint,
2278    after: bool,
2279    target: char,
2280    times: usize,
2281    mode: FindRange,
2282    smartcase: bool,
2283) -> DisplayPoint {
2284    let mut to = from;
2285
2286    for _ in 0..times {
2287        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2288            is_character_match(target, right, smartcase)
2289        });
2290        if to == new_to {
2291            break;
2292        }
2293        to = new_to;
2294    }
2295
2296    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2297    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2298        if after {
2299            *to.column_mut() += 1;
2300            map.clip_point(to, Bias::Right)
2301        } else {
2302            to
2303        }
2304    } else {
2305        from
2306    }
2307}
2308
2309fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2310    if smartcase {
2311        if target.is_uppercase() {
2312            target == other
2313        } else {
2314            target == other.to_ascii_lowercase()
2315        }
2316    } else {
2317        target == other
2318    }
2319}
2320
2321fn sneak(
2322    map: &DisplaySnapshot,
2323    from: DisplayPoint,
2324    first_target: char,
2325    second_target: char,
2326    times: usize,
2327    smartcase: bool,
2328) -> Option<DisplayPoint> {
2329    let mut to = from;
2330    let mut found = false;
2331
2332    for _ in 0..times {
2333        found = false;
2334        let new_to = find_boundary(
2335            map,
2336            movement::right(map, to),
2337            FindRange::MultiLine,
2338            |left, right| {
2339                found = is_character_match(first_target, left, smartcase)
2340                    && is_character_match(second_target, right, smartcase);
2341                found
2342            },
2343        );
2344        if to == new_to {
2345            break;
2346        }
2347        to = new_to;
2348    }
2349
2350    if found {
2351        Some(movement::left(map, to))
2352    } else {
2353        None
2354    }
2355}
2356
2357fn sneak_backward(
2358    map: &DisplaySnapshot,
2359    from: DisplayPoint,
2360    first_target: char,
2361    second_target: char,
2362    times: usize,
2363    smartcase: bool,
2364) -> Option<DisplayPoint> {
2365    let mut to = from;
2366    let mut found = false;
2367
2368    for _ in 0..times {
2369        found = false;
2370        let new_to =
2371            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2372                found = is_character_match(first_target, left, smartcase)
2373                    && is_character_match(second_target, right, smartcase);
2374                found
2375            });
2376        if to == new_to {
2377            break;
2378        }
2379        to = new_to;
2380    }
2381
2382    if found {
2383        Some(movement::left(map, to))
2384    } else {
2385        None
2386    }
2387}
2388
2389fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2390    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2391    first_non_whitespace(map, false, correct_line)
2392}
2393
2394fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2395    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2396    first_non_whitespace(map, false, correct_line)
2397}
2398
2399fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2400    let correct_line = start_of_relative_buffer_row(map, point, 0);
2401    right(map, correct_line, times.saturating_sub(1))
2402}
2403
2404pub(crate) fn next_line_end(
2405    map: &DisplaySnapshot,
2406    mut point: DisplayPoint,
2407    times: usize,
2408) -> DisplayPoint {
2409    if times > 1 {
2410        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2411    }
2412    end_of_line(map, false, point, 1)
2413}
2414
2415fn window_top(
2416    map: &DisplaySnapshot,
2417    point: DisplayPoint,
2418    text_layout_details: &TextLayoutDetails,
2419    mut times: usize,
2420) -> (DisplayPoint, SelectionGoal) {
2421    let first_visible_line = text_layout_details
2422        .scroll_anchor
2423        .anchor
2424        .to_display_point(map);
2425
2426    if first_visible_line.row() != DisplayRow(0)
2427        && text_layout_details.vertical_scroll_margin as usize > times
2428    {
2429        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2430    }
2431
2432    if let Some(visible_rows) = text_layout_details.visible_rows {
2433        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2434        let new_row = (first_visible_line.row().0 + (times as u32))
2435            .min(bottom_row)
2436            .min(map.max_point().row().0);
2437        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2438
2439        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2440        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2441    } else {
2442        let new_row =
2443            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2444        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2445
2446        let new_point = DisplayPoint::new(new_row, new_col);
2447        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2448    }
2449}
2450
2451fn window_middle(
2452    map: &DisplaySnapshot,
2453    point: DisplayPoint,
2454    text_layout_details: &TextLayoutDetails,
2455) -> (DisplayPoint, SelectionGoal) {
2456    if let Some(visible_rows) = text_layout_details.visible_rows {
2457        let first_visible_line = text_layout_details
2458            .scroll_anchor
2459            .anchor
2460            .to_display_point(map);
2461
2462        let max_visible_rows =
2463            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2464
2465        let new_row =
2466            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2467        let new_row = DisplayRow(new_row);
2468        let new_col = point.column().min(map.line_len(new_row));
2469        let new_point = DisplayPoint::new(new_row, new_col);
2470        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2471    } else {
2472        (point, SelectionGoal::None)
2473    }
2474}
2475
2476fn window_bottom(
2477    map: &DisplaySnapshot,
2478    point: DisplayPoint,
2479    text_layout_details: &TextLayoutDetails,
2480    mut times: usize,
2481) -> (DisplayPoint, SelectionGoal) {
2482    if let Some(visible_rows) = text_layout_details.visible_rows {
2483        let first_visible_line = text_layout_details
2484            .scroll_anchor
2485            .anchor
2486            .to_display_point(map);
2487        let bottom_row = first_visible_line.row().0
2488            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2489        if bottom_row < map.max_point().row().0
2490            && text_layout_details.vertical_scroll_margin as usize > times
2491        {
2492            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2493        }
2494        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2495        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2496        {
2497            first_visible_line.row()
2498        } else {
2499            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2500        };
2501        let new_col = point.column().min(map.line_len(new_row));
2502        let new_point = DisplayPoint::new(new_row, new_col);
2503        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2504    } else {
2505        (point, SelectionGoal::None)
2506    }
2507}
2508
2509fn method_motion(
2510    map: &DisplaySnapshot,
2511    mut display_point: DisplayPoint,
2512    times: usize,
2513    direction: Direction,
2514    is_start: bool,
2515) -> DisplayPoint {
2516    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2517        return display_point;
2518    };
2519
2520    for _ in 0..times {
2521        let point = map.display_point_to_point(display_point, Bias::Left);
2522        let offset = point.to_offset(&map.buffer_snapshot);
2523        let range = if direction == Direction::Prev {
2524            0..offset
2525        } else {
2526            offset..buffer.len()
2527        };
2528
2529        let possibilities = buffer
2530            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2531            .filter_map(|(range, object)| {
2532                if !matches!(object, language::TextObject::AroundFunction) {
2533                    return None;
2534                }
2535
2536                let relevant = if is_start { range.start } else { range.end };
2537                if direction == Direction::Prev && relevant < offset {
2538                    Some(relevant)
2539                } else if direction == Direction::Next && relevant > offset + 1 {
2540                    Some(relevant)
2541                } else {
2542                    None
2543                }
2544            });
2545
2546        let dest = if direction == Direction::Prev {
2547            possibilities.max().unwrap_or(offset)
2548        } else {
2549            possibilities.min().unwrap_or(offset)
2550        };
2551        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2552        if new_point == display_point {
2553            break;
2554        }
2555        display_point = new_point;
2556    }
2557    display_point
2558}
2559
2560fn comment_motion(
2561    map: &DisplaySnapshot,
2562    mut display_point: DisplayPoint,
2563    times: usize,
2564    direction: Direction,
2565) -> DisplayPoint {
2566    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2567        return display_point;
2568    };
2569
2570    for _ in 0..times {
2571        let point = map.display_point_to_point(display_point, Bias::Left);
2572        let offset = point.to_offset(&map.buffer_snapshot);
2573        let range = if direction == Direction::Prev {
2574            0..offset
2575        } else {
2576            offset..buffer.len()
2577        };
2578
2579        let possibilities = buffer
2580            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2581            .filter_map(|(range, object)| {
2582                if !matches!(object, language::TextObject::AroundComment) {
2583                    return None;
2584                }
2585
2586                let relevant = if direction == Direction::Prev {
2587                    range.start
2588                } else {
2589                    range.end
2590                };
2591                if direction == Direction::Prev && relevant < offset {
2592                    Some(relevant)
2593                } else if direction == Direction::Next && relevant > offset + 1 {
2594                    Some(relevant)
2595                } else {
2596                    None
2597                }
2598            });
2599
2600        let dest = if direction == Direction::Prev {
2601            possibilities.max().unwrap_or(offset)
2602        } else {
2603            possibilities.min().unwrap_or(offset)
2604        };
2605        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2606        if new_point == display_point {
2607            break;
2608        }
2609        display_point = new_point;
2610    }
2611
2612    display_point
2613}
2614
2615fn section_motion(
2616    map: &DisplaySnapshot,
2617    mut display_point: DisplayPoint,
2618    times: usize,
2619    direction: Direction,
2620    is_start: bool,
2621) -> DisplayPoint {
2622    if map.buffer_snapshot.as_singleton().is_some() {
2623        for _ in 0..times {
2624            let offset = map
2625                .display_point_to_point(display_point, Bias::Left)
2626                .to_offset(&map.buffer_snapshot);
2627            let range = if direction == Direction::Prev {
2628                0..offset
2629            } else {
2630                offset..map.buffer_snapshot.len()
2631            };
2632
2633            // we set a max start depth here because we want a section to only be "top level"
2634            // similar to vim's default of '{' in the first column.
2635            // (and without it, ]] at the start of editor.rs is -very- slow)
2636            let mut possibilities = map
2637                .buffer_snapshot
2638                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2639                .filter(|(_, object)| {
2640                    matches!(
2641                        object,
2642                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2643                    )
2644                })
2645                .collect::<Vec<_>>();
2646            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2647            let mut prev_end = None;
2648            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2649                if t == language::TextObject::AroundFunction
2650                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2651                {
2652                    return None;
2653                }
2654                prev_end = Some(range.end);
2655
2656                let relevant = if is_start { range.start } else { range.end };
2657                if direction == Direction::Prev && relevant < offset {
2658                    Some(relevant)
2659                } else if direction == Direction::Next && relevant > offset + 1 {
2660                    Some(relevant)
2661                } else {
2662                    None
2663                }
2664            });
2665
2666            let offset = if direction == Direction::Prev {
2667                possibilities.max().unwrap_or(0)
2668            } else {
2669                possibilities.min().unwrap_or(map.buffer_snapshot.len())
2670            };
2671
2672            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2673            if new_point == display_point {
2674                break;
2675            }
2676            display_point = new_point;
2677        }
2678        return display_point;
2679    };
2680
2681    for _ in 0..times {
2682        let point = map.display_point_to_point(display_point, Bias::Left);
2683        let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2684            return display_point;
2685        };
2686        let next_point = match (direction, is_start) {
2687            (Direction::Prev, true) => {
2688                let mut start = excerpt.start_anchor().to_display_point(&map);
2689                if start >= display_point && start.row() > DisplayRow(0) {
2690                    let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2691                        return display_point;
2692                    };
2693                    start = excerpt.start_anchor().to_display_point(&map);
2694                }
2695                start
2696            }
2697            (Direction::Prev, false) => {
2698                let mut start = excerpt.start_anchor().to_display_point(&map);
2699                if start.row() > DisplayRow(0) {
2700                    *start.row_mut() -= 1;
2701                }
2702                map.clip_point(start, Bias::Left)
2703            }
2704            (Direction::Next, true) => {
2705                let mut end = excerpt.end_anchor().to_display_point(&map);
2706                *end.row_mut() += 1;
2707                map.clip_point(end, Bias::Right)
2708            }
2709            (Direction::Next, false) => {
2710                let mut end = excerpt.end_anchor().to_display_point(&map);
2711                *end.column_mut() = 0;
2712                if end <= display_point {
2713                    *end.row_mut() += 1;
2714                    let point_end = map.display_point_to_point(end, Bias::Right);
2715                    let Some(excerpt) =
2716                        map.buffer_snapshot.excerpt_containing(point_end..point_end)
2717                    else {
2718                        return display_point;
2719                    };
2720                    end = excerpt.end_anchor().to_display_point(&map);
2721                    *end.column_mut() = 0;
2722                }
2723                end
2724            }
2725        };
2726        if next_point == display_point {
2727            break;
2728        }
2729        display_point = next_point;
2730    }
2731
2732    display_point
2733}
2734
2735#[cfg(test)]
2736mod test {
2737
2738    use crate::{
2739        state::Mode,
2740        test::{NeovimBackedTestContext, VimTestContext},
2741    };
2742    use editor::display_map::Inlay;
2743    use indoc::indoc;
2744
2745    #[gpui::test]
2746    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2747        let mut cx = NeovimBackedTestContext::new(cx).await;
2748
2749        let initial_state = indoc! {r"ˇabc
2750            def
2751
2752            paragraph
2753            the second
2754
2755
2756
2757            third and
2758            final"};
2759
2760        // goes down once
2761        cx.set_shared_state(initial_state).await;
2762        cx.simulate_shared_keystrokes("}").await;
2763        cx.shared_state().await.assert_eq(indoc! {r"abc
2764            def
2765            ˇ
2766            paragraph
2767            the second
2768
2769
2770
2771            third and
2772            final"});
2773
2774        // goes up once
2775        cx.simulate_shared_keystrokes("{").await;
2776        cx.shared_state().await.assert_eq(initial_state);
2777
2778        // goes down twice
2779        cx.simulate_shared_keystrokes("2 }").await;
2780        cx.shared_state().await.assert_eq(indoc! {r"abc
2781            def
2782
2783            paragraph
2784            the second
2785            ˇ
2786
2787
2788            third and
2789            final"});
2790
2791        // goes down over multiple blanks
2792        cx.simulate_shared_keystrokes("}").await;
2793        cx.shared_state().await.assert_eq(indoc! {r"abc
2794                def
2795
2796                paragraph
2797                the second
2798
2799
2800
2801                third and
2802                finaˇl"});
2803
2804        // goes up twice
2805        cx.simulate_shared_keystrokes("2 {").await;
2806        cx.shared_state().await.assert_eq(indoc! {r"abc
2807                def
2808                ˇ
2809                paragraph
2810                the second
2811
2812
2813
2814                third and
2815                final"});
2816    }
2817
2818    #[gpui::test]
2819    async fn test_matching(cx: &mut gpui::TestAppContext) {
2820        let mut cx = NeovimBackedTestContext::new(cx).await;
2821
2822        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2823                do(something(with<Types>.and_arrays[0, 2]))
2824            }"})
2825            .await;
2826        cx.simulate_shared_keystrokes("%").await;
2827        cx.shared_state()
2828            .await
2829            .assert_eq(indoc! {r"func (a stringˇ) {
2830                do(something(with<Types>.and_arrays[0, 2]))
2831            }"});
2832
2833        // test it works on the last character of the line
2834        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2835            do(something(with<Types>.and_arrays[0, 2]))
2836            }"})
2837            .await;
2838        cx.simulate_shared_keystrokes("%").await;
2839        cx.shared_state()
2840            .await
2841            .assert_eq(indoc! {r"func (a string) {
2842            do(something(with<Types>.and_arrays[0, 2]))
2843            ˇ}"});
2844
2845        // test it works on immediate nesting
2846        cx.set_shared_state("ˇ{()}").await;
2847        cx.simulate_shared_keystrokes("%").await;
2848        cx.shared_state().await.assert_eq("{()ˇ}");
2849        cx.simulate_shared_keystrokes("%").await;
2850        cx.shared_state().await.assert_eq("ˇ{()}");
2851
2852        // test it works on immediate nesting inside braces
2853        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2854        cx.simulate_shared_keystrokes("%").await;
2855        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2856
2857        // test it jumps to the next paren on a line
2858        cx.set_shared_state("func ˇboop() {\n}").await;
2859        cx.simulate_shared_keystrokes("%").await;
2860        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2861    }
2862
2863    #[gpui::test]
2864    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2865        let mut cx = NeovimBackedTestContext::new(cx).await;
2866
2867        // test it works with curly braces
2868        cx.set_shared_state(indoc! {r"func (a string) {
2869                do(something(with<Types>.anˇd_arrays[0, 2]))
2870            }"})
2871            .await;
2872        cx.simulate_shared_keystrokes("] }").await;
2873        cx.shared_state()
2874            .await
2875            .assert_eq(indoc! {r"func (a string) {
2876                do(something(with<Types>.and_arrays[0, 2]))
2877            ˇ}"});
2878
2879        // test it works with brackets
2880        cx.set_shared_state(indoc! {r"func (a string) {
2881                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2882            }"})
2883            .await;
2884        cx.simulate_shared_keystrokes("] )").await;
2885        cx.shared_state()
2886            .await
2887            .assert_eq(indoc! {r"func (a string) {
2888                do(something(with<Types>.and_arrays[0, 2])ˇ)
2889            }"});
2890
2891        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2892            .await;
2893        cx.simulate_shared_keystrokes("] )").await;
2894        cx.shared_state()
2895            .await
2896            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2897
2898        // test it works on immediate nesting
2899        cx.set_shared_state("{ˇ {}{}}").await;
2900        cx.simulate_shared_keystrokes("] }").await;
2901        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2902        cx.set_shared_state("(ˇ ()())").await;
2903        cx.simulate_shared_keystrokes("] )").await;
2904        cx.shared_state().await.assert_eq("( ()()ˇ)");
2905
2906        // test it works on immediate nesting inside braces
2907        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2908        cx.simulate_shared_keystrokes("] }").await;
2909        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2910        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2911        cx.simulate_shared_keystrokes("] )").await;
2912        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2913    }
2914
2915    #[gpui::test]
2916    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2917        let mut cx = NeovimBackedTestContext::new(cx).await;
2918
2919        // test it works with curly braces
2920        cx.set_shared_state(indoc! {r"func (a string) {
2921                do(something(with<Types>.anˇd_arrays[0, 2]))
2922            }"})
2923            .await;
2924        cx.simulate_shared_keystrokes("[ {").await;
2925        cx.shared_state()
2926            .await
2927            .assert_eq(indoc! {r"func (a string) ˇ{
2928                do(something(with<Types>.and_arrays[0, 2]))
2929            }"});
2930
2931        // test it works with brackets
2932        cx.set_shared_state(indoc! {r"func (a string) {
2933                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2934            }"})
2935            .await;
2936        cx.simulate_shared_keystrokes("[ (").await;
2937        cx.shared_state()
2938            .await
2939            .assert_eq(indoc! {r"func (a string) {
2940                doˇ(something(with<Types>.and_arrays[0, 2]))
2941            }"});
2942
2943        // test it works on immediate nesting
2944        cx.set_shared_state("{{}{} ˇ }").await;
2945        cx.simulate_shared_keystrokes("[ {").await;
2946        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2947        cx.set_shared_state("(()() ˇ )").await;
2948        cx.simulate_shared_keystrokes("[ (").await;
2949        cx.shared_state().await.assert_eq("ˇ(()()  )");
2950
2951        // test it works on immediate nesting inside braces
2952        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2953        cx.simulate_shared_keystrokes("[ {").await;
2954        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2955        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2956        cx.simulate_shared_keystrokes("[ (").await;
2957        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2958    }
2959
2960    #[gpui::test]
2961    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2962        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2963
2964        cx.neovim.exec("set filetype=html").await;
2965
2966        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2967        cx.simulate_shared_keystrokes("%").await;
2968        cx.shared_state()
2969            .await
2970            .assert_eq(indoc! {r"<body><ˇ/body>"});
2971        cx.simulate_shared_keystrokes("%").await;
2972
2973        // test jumping backwards
2974        cx.shared_state()
2975            .await
2976            .assert_eq(indoc! {r"<ˇbody></body>"});
2977
2978        // test self-closing tags
2979        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2980        cx.simulate_shared_keystrokes("%").await;
2981        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2982
2983        // test tag with attributes
2984        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2985            </div>
2986            "})
2987            .await;
2988        cx.simulate_shared_keystrokes("%").await;
2989        cx.shared_state()
2990            .await
2991            .assert_eq(indoc! {r"<div class='test' id='main'>
2992            <ˇ/div>
2993            "});
2994
2995        // test multi-line self-closing tag
2996        cx.set_shared_state(indoc! {r#"<a>
2997            <br
2998                test = "test"
2999            /ˇ>
3000        </a>"#})
3001            .await;
3002        cx.simulate_shared_keystrokes("%").await;
3003        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3004            ˇ<br
3005                test = "test"
3006            />
3007        </a>"#});
3008    }
3009
3010    #[gpui::test]
3011    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3012        let mut cx = NeovimBackedTestContext::new(cx).await;
3013
3014        // f and F
3015        cx.set_shared_state("ˇone two three four").await;
3016        cx.simulate_shared_keystrokes("f o").await;
3017        cx.shared_state().await.assert_eq("one twˇo three four");
3018        cx.simulate_shared_keystrokes(",").await;
3019        cx.shared_state().await.assert_eq("ˇone two three four");
3020        cx.simulate_shared_keystrokes("2 ;").await;
3021        cx.shared_state().await.assert_eq("one two three fˇour");
3022        cx.simulate_shared_keystrokes("shift-f e").await;
3023        cx.shared_state().await.assert_eq("one two threˇe four");
3024        cx.simulate_shared_keystrokes("2 ;").await;
3025        cx.shared_state().await.assert_eq("onˇe two three four");
3026        cx.simulate_shared_keystrokes(",").await;
3027        cx.shared_state().await.assert_eq("one two thrˇee four");
3028
3029        // t and T
3030        cx.set_shared_state("ˇone two three four").await;
3031        cx.simulate_shared_keystrokes("t o").await;
3032        cx.shared_state().await.assert_eq("one tˇwo three four");
3033        cx.simulate_shared_keystrokes(",").await;
3034        cx.shared_state().await.assert_eq("oˇne two three four");
3035        cx.simulate_shared_keystrokes("2 ;").await;
3036        cx.shared_state().await.assert_eq("one two three ˇfour");
3037        cx.simulate_shared_keystrokes("shift-t e").await;
3038        cx.shared_state().await.assert_eq("one two threeˇ four");
3039        cx.simulate_shared_keystrokes("3 ;").await;
3040        cx.shared_state().await.assert_eq("oneˇ two three four");
3041        cx.simulate_shared_keystrokes(",").await;
3042        cx.shared_state().await.assert_eq("one two thˇree four");
3043    }
3044
3045    #[gpui::test]
3046    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3047        let mut cx = NeovimBackedTestContext::new(cx).await;
3048        let initial_state = indoc! {r"something(ˇfoo)"};
3049        cx.set_shared_state(initial_state).await;
3050        cx.simulate_shared_keystrokes("}").await;
3051        cx.shared_state().await.assert_eq("something(fooˇ)");
3052    }
3053
3054    #[gpui::test]
3055    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3056        let mut cx = NeovimBackedTestContext::new(cx).await;
3057        cx.set_shared_state("ˇone\n  two\nthree").await;
3058        cx.simulate_shared_keystrokes("enter").await;
3059        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3060    }
3061
3062    #[gpui::test]
3063    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3064        let mut cx = NeovimBackedTestContext::new(cx).await;
3065        cx.set_shared_state("ˇ one\n two \nthree").await;
3066        cx.simulate_shared_keystrokes("g _").await;
3067        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3068
3069        cx.set_shared_state("ˇ one \n two \nthree").await;
3070        cx.simulate_shared_keystrokes("g _").await;
3071        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3072        cx.simulate_shared_keystrokes("2 g _").await;
3073        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3074    }
3075
3076    #[gpui::test]
3077    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3078        let mut cx = NeovimBackedTestContext::new(cx).await;
3079        let initial_state = indoc! {r"abc
3080          def
3081          paragraph
3082          the second
3083          third ˇand
3084          final"};
3085
3086        cx.set_shared_state(initial_state).await;
3087        cx.simulate_shared_keystrokes("shift-h").await;
3088        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3089          def
3090          paragraph
3091          the second
3092          third and
3093          final"});
3094
3095        // clip point
3096        cx.set_shared_state(indoc! {r"
3097          1 2 3
3098          4 5 6
3099          7 8 ˇ9
3100          "})
3101            .await;
3102        cx.simulate_shared_keystrokes("shift-h").await;
3103        cx.shared_state().await.assert_eq(indoc! {"
3104          1 2 ˇ3
3105          4 5 6
3106          7 8 9
3107          "});
3108
3109        cx.set_shared_state(indoc! {r"
3110          1 2 3
3111          4 5 6
3112          ˇ7 8 9
3113          "})
3114            .await;
3115        cx.simulate_shared_keystrokes("shift-h").await;
3116        cx.shared_state().await.assert_eq(indoc! {"
3117          ˇ1 2 3
3118          4 5 6
3119          7 8 9
3120          "});
3121
3122        cx.set_shared_state(indoc! {r"
3123          1 2 3
3124          4 5 ˇ6
3125          7 8 9"})
3126            .await;
3127        cx.simulate_shared_keystrokes("9 shift-h").await;
3128        cx.shared_state().await.assert_eq(indoc! {"
3129          1 2 3
3130          4 5 6
3131          7 8 ˇ9"});
3132    }
3133
3134    #[gpui::test]
3135    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3136        let mut cx = NeovimBackedTestContext::new(cx).await;
3137        let initial_state = indoc! {r"abˇc
3138          def
3139          paragraph
3140          the second
3141          third and
3142          final"};
3143
3144        cx.set_shared_state(initial_state).await;
3145        cx.simulate_shared_keystrokes("shift-m").await;
3146        cx.shared_state().await.assert_eq(indoc! {r"abc
3147          def
3148          paˇragraph
3149          the second
3150          third and
3151          final"});
3152
3153        cx.set_shared_state(indoc! {r"
3154          1 2 3
3155          4 5 6
3156          7 8 ˇ9
3157          "})
3158            .await;
3159        cx.simulate_shared_keystrokes("shift-m").await;
3160        cx.shared_state().await.assert_eq(indoc! {"
3161          1 2 3
3162          4 5 ˇ6
3163          7 8 9
3164          "});
3165        cx.set_shared_state(indoc! {r"
3166          1 2 3
3167          4 5 6
3168          ˇ7 8 9
3169          "})
3170            .await;
3171        cx.simulate_shared_keystrokes("shift-m").await;
3172        cx.shared_state().await.assert_eq(indoc! {"
3173          1 2 3
3174          ˇ4 5 6
3175          7 8 9
3176          "});
3177        cx.set_shared_state(indoc! {r"
3178          ˇ1 2 3
3179          4 5 6
3180          7 8 9
3181          "})
3182            .await;
3183        cx.simulate_shared_keystrokes("shift-m").await;
3184        cx.shared_state().await.assert_eq(indoc! {"
3185          1 2 3
3186          ˇ4 5 6
3187          7 8 9
3188          "});
3189        cx.set_shared_state(indoc! {r"
3190          1 2 3
3191          ˇ4 5 6
3192          7 8 9
3193          "})
3194            .await;
3195        cx.simulate_shared_keystrokes("shift-m").await;
3196        cx.shared_state().await.assert_eq(indoc! {"
3197          1 2 3
3198          ˇ4 5 6
3199          7 8 9
3200          "});
3201        cx.set_shared_state(indoc! {r"
3202          1 2 3
3203          4 5 ˇ6
3204          7 8 9
3205          "})
3206            .await;
3207        cx.simulate_shared_keystrokes("shift-m").await;
3208        cx.shared_state().await.assert_eq(indoc! {"
3209          1 2 3
3210          4 5 ˇ6
3211          7 8 9
3212          "});
3213    }
3214
3215    #[gpui::test]
3216    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3217        let mut cx = NeovimBackedTestContext::new(cx).await;
3218        let initial_state = indoc! {r"abc
3219          deˇf
3220          paragraph
3221          the second
3222          third and
3223          final"};
3224
3225        cx.set_shared_state(initial_state).await;
3226        cx.simulate_shared_keystrokes("shift-l").await;
3227        cx.shared_state().await.assert_eq(indoc! {r"abc
3228          def
3229          paragraph
3230          the second
3231          third and
3232          fiˇnal"});
3233
3234        cx.set_shared_state(indoc! {r"
3235          1 2 3
3236          4 5 ˇ6
3237          7 8 9
3238          "})
3239            .await;
3240        cx.simulate_shared_keystrokes("shift-l").await;
3241        cx.shared_state().await.assert_eq(indoc! {"
3242          1 2 3
3243          4 5 6
3244          7 8 9
3245          ˇ"});
3246
3247        cx.set_shared_state(indoc! {r"
3248          1 2 3
3249          ˇ4 5 6
3250          7 8 9
3251          "})
3252            .await;
3253        cx.simulate_shared_keystrokes("shift-l").await;
3254        cx.shared_state().await.assert_eq(indoc! {"
3255          1 2 3
3256          4 5 6
3257          7 8 9
3258          ˇ"});
3259
3260        cx.set_shared_state(indoc! {r"
3261          1 2 ˇ3
3262          4 5 6
3263          7 8 9
3264          "})
3265            .await;
3266        cx.simulate_shared_keystrokes("shift-l").await;
3267        cx.shared_state().await.assert_eq(indoc! {"
3268          1 2 3
3269          4 5 6
3270          7 8 9
3271          ˇ"});
3272
3273        cx.set_shared_state(indoc! {r"
3274          ˇ1 2 3
3275          4 5 6
3276          7 8 9
3277          "})
3278            .await;
3279        cx.simulate_shared_keystrokes("shift-l").await;
3280        cx.shared_state().await.assert_eq(indoc! {"
3281          1 2 3
3282          4 5 6
3283          7 8 9
3284          ˇ"});
3285
3286        cx.set_shared_state(indoc! {r"
3287          1 2 3
3288          4 5 ˇ6
3289          7 8 9
3290          "})
3291            .await;
3292        cx.simulate_shared_keystrokes("9 shift-l").await;
3293        cx.shared_state().await.assert_eq(indoc! {"
3294          1 2 ˇ3
3295          4 5 6
3296          7 8 9
3297          "});
3298    }
3299
3300    #[gpui::test]
3301    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3302        let mut cx = NeovimBackedTestContext::new(cx).await;
3303        cx.set_shared_state(indoc! {r"
3304        456 5ˇ67 678
3305        "})
3306            .await;
3307        cx.simulate_shared_keystrokes("g e").await;
3308        cx.shared_state().await.assert_eq(indoc! {"
3309        45ˇ6 567 678
3310        "});
3311
3312        // Test times
3313        cx.set_shared_state(indoc! {r"
3314        123 234 345
3315        456 5ˇ67 678
3316        "})
3317            .await;
3318        cx.simulate_shared_keystrokes("4 g e").await;
3319        cx.shared_state().await.assert_eq(indoc! {"
3320        12ˇ3 234 345
3321        456 567 678
3322        "});
3323
3324        // With punctuation
3325        cx.set_shared_state(indoc! {r"
3326        123 234 345
3327        4;5.6 5ˇ67 678
3328        789 890 901
3329        "})
3330            .await;
3331        cx.simulate_shared_keystrokes("g e").await;
3332        cx.shared_state().await.assert_eq(indoc! {"
3333          123 234 345
3334          4;5.ˇ6 567 678
3335          789 890 901
3336        "});
3337
3338        // With punctuation and count
3339        cx.set_shared_state(indoc! {r"
3340        123 234 345
3341        4;5.6 5ˇ67 678
3342        789 890 901
3343        "})
3344            .await;
3345        cx.simulate_shared_keystrokes("5 g e").await;
3346        cx.shared_state().await.assert_eq(indoc! {"
3347          123 234 345
3348          ˇ4;5.6 567 678
3349          789 890 901
3350        "});
3351
3352        // newlines
3353        cx.set_shared_state(indoc! {r"
3354        123 234 345
3355
3356        78ˇ9 890 901
3357        "})
3358            .await;
3359        cx.simulate_shared_keystrokes("g e").await;
3360        cx.shared_state().await.assert_eq(indoc! {"
3361          123 234 345
3362          ˇ
3363          789 890 901
3364        "});
3365        cx.simulate_shared_keystrokes("g e").await;
3366        cx.shared_state().await.assert_eq(indoc! {"
3367          123 234 34ˇ5
3368
3369          789 890 901
3370        "});
3371
3372        // With punctuation
3373        cx.set_shared_state(indoc! {r"
3374        123 234 345
3375        4;5.ˇ6 567 678
3376        789 890 901
3377        "})
3378            .await;
3379        cx.simulate_shared_keystrokes("g shift-e").await;
3380        cx.shared_state().await.assert_eq(indoc! {"
3381          123 234 34ˇ5
3382          4;5.6 567 678
3383          789 890 901
3384        "});
3385    }
3386
3387    #[gpui::test]
3388    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3389        let mut cx = NeovimBackedTestContext::new(cx).await;
3390
3391        cx.set_shared_state(indoc! {"
3392            fn aˇ() {
3393              return
3394            }
3395        "})
3396            .await;
3397        cx.simulate_shared_keystrokes("v $ %").await;
3398        cx.shared_state().await.assert_eq(indoc! {"
3399            fn a«() {
3400              return
3401            }ˇ»
3402        "});
3403    }
3404
3405    #[gpui::test]
3406    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3407        let mut cx = VimTestContext::new(cx, true).await;
3408
3409        cx.set_state(
3410            indoc! {"
3411            struct Foo {
3412            ˇ
3413            }
3414        "},
3415            Mode::Normal,
3416        );
3417
3418        cx.update_editor(|editor, cx| {
3419            let range = editor.selections.newest_anchor().range();
3420            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3421            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3422            editor.splice_inlays(vec![], vec![inlay], cx);
3423        });
3424
3425        cx.simulate_keystrokes("j");
3426        cx.assert_state(
3427            indoc! {"
3428            struct Foo {
3429
3430            ˇ}
3431        "},
3432            Mode::Normal,
3433        );
3434    }
3435}