motion.rs

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