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