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    if let Some((opening_range, closing_range)) = map
2283        .buffer_snapshot
2284        .innermost_enclosing_bracket_ranges(offset..offset, None)
2285    {
2286        if opening_range.contains(&offset) {
2287            return closing_range.start.to_display_point(map);
2288        } else if closing_range.contains(&offset) {
2289            return opening_range.start.to_display_point(map);
2290        }
2291    }
2292
2293    let line_range = map.prev_line_boundary(point).0..line_end;
2294    let visible_line_range =
2295        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2296    let ranges = map
2297        .buffer_snapshot
2298        .bracket_ranges(visible_line_range.clone());
2299    if let Some(ranges) = ranges {
2300        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2301            ..line_range.end.to_offset(&map.buffer_snapshot);
2302        let mut closest_pair_destination = None;
2303        let mut closest_distance = usize::MAX;
2304
2305        for (open_range, close_range) in ranges {
2306            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2307                if offset > open_range.start && offset < close_range.start {
2308                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2309                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2310                        return display_point;
2311                    }
2312                    if let Some(tag) = matching_tag(map, display_point) {
2313                        return tag;
2314                    }
2315                } else if close_range.contains(&offset) {
2316                    return open_range.start.to_display_point(map);
2317                } else if open_range.contains(&offset) {
2318                    return (close_range.end - 1).to_display_point(map);
2319                }
2320            }
2321
2322            if (open_range.contains(&offset) || open_range.start >= offset)
2323                && line_range.contains(&open_range.start)
2324            {
2325                let distance = open_range.start.saturating_sub(offset);
2326                if distance < closest_distance {
2327                    closest_pair_destination = Some(close_range.start);
2328                    closest_distance = distance;
2329                    continue;
2330                }
2331            }
2332
2333            if (close_range.contains(&offset) || close_range.start >= offset)
2334                && line_range.contains(&close_range.start)
2335            {
2336                let distance = close_range.start.saturating_sub(offset);
2337                if distance < closest_distance {
2338                    closest_pair_destination = Some(open_range.start);
2339                    closest_distance = distance;
2340                    continue;
2341                }
2342            }
2343
2344            continue;
2345        }
2346
2347        closest_pair_destination
2348            .map(|destination| destination.to_display_point(map))
2349            .unwrap_or(display_point)
2350    } else {
2351        display_point
2352    }
2353}
2354
2355// Go to {count} percentage in the file, on the first
2356// non-blank in the line linewise.  To compute the new
2357// line number this formula is used:
2358// ({count} * number-of-lines + 99) / 100
2359//
2360// https://neovim.io/doc/user/motion.html#N%25
2361fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2362    let total_lines = map.buffer_snapshot.max_point().row + 1;
2363    let target_line = (count * total_lines as usize).div_ceil(100);
2364    let target_point = DisplayPoint::new(
2365        DisplayRow(target_line.saturating_sub(1) as u32),
2366        point.column(),
2367    );
2368    map.clip_point(target_point, Bias::Left)
2369}
2370
2371fn unmatched_forward(
2372    map: &DisplaySnapshot,
2373    mut display_point: DisplayPoint,
2374    char: char,
2375    times: usize,
2376) -> DisplayPoint {
2377    for _ in 0..times {
2378        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2379        let point = display_point.to_point(map);
2380        let offset = point.to_offset(&map.buffer_snapshot);
2381
2382        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2383        let Some(ranges) = ranges else { break };
2384        let mut closest_closing_destination = None;
2385        let mut closest_distance = usize::MAX;
2386
2387        for (_, close_range) in ranges {
2388            if close_range.start > offset {
2389                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2390                if Some(char) == chars.next() {
2391                    let distance = close_range.start - offset;
2392                    if distance < closest_distance {
2393                        closest_closing_destination = Some(close_range.start);
2394                        closest_distance = distance;
2395                        continue;
2396                    }
2397                }
2398            }
2399        }
2400
2401        let new_point = closest_closing_destination
2402            .map(|destination| destination.to_display_point(map))
2403            .unwrap_or(display_point);
2404        if new_point == display_point {
2405            break;
2406        }
2407        display_point = new_point;
2408    }
2409    return display_point;
2410}
2411
2412fn unmatched_backward(
2413    map: &DisplaySnapshot,
2414    mut display_point: DisplayPoint,
2415    char: char,
2416    times: usize,
2417) -> DisplayPoint {
2418    for _ in 0..times {
2419        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2420        let point = display_point.to_point(map);
2421        let offset = point.to_offset(&map.buffer_snapshot);
2422
2423        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2424        let Some(ranges) = ranges else {
2425            break;
2426        };
2427
2428        let mut closest_starting_destination = None;
2429        let mut closest_distance = usize::MAX;
2430
2431        for (start_range, _) in ranges {
2432            if start_range.start < offset {
2433                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2434                if Some(char) == chars.next() {
2435                    let distance = offset - start_range.start;
2436                    if distance < closest_distance {
2437                        closest_starting_destination = Some(start_range.start);
2438                        closest_distance = distance;
2439                        continue;
2440                    }
2441                }
2442            }
2443        }
2444
2445        let new_point = closest_starting_destination
2446            .map(|destination| destination.to_display_point(map))
2447            .unwrap_or(display_point);
2448        if new_point == display_point {
2449            break;
2450        } else {
2451            display_point = new_point;
2452        }
2453    }
2454    display_point
2455}
2456
2457fn find_forward(
2458    map: &DisplaySnapshot,
2459    from: DisplayPoint,
2460    before: bool,
2461    target: char,
2462    times: usize,
2463    mode: FindRange,
2464    smartcase: bool,
2465) -> Option<DisplayPoint> {
2466    let mut to = from;
2467    let mut found = false;
2468
2469    for _ in 0..times {
2470        found = false;
2471        let new_to = find_boundary(map, to, mode, |_, right| {
2472            found = is_character_match(target, right, smartcase);
2473            found
2474        });
2475        if to == new_to {
2476            break;
2477        }
2478        to = new_to;
2479    }
2480
2481    if found {
2482        if before && to.column() > 0 {
2483            *to.column_mut() -= 1;
2484            Some(map.clip_point(to, Bias::Left))
2485        } else if before && to.row().0 > 0 {
2486            *to.row_mut() -= 1;
2487            *to.column_mut() = map.line(to.row()).len() as u32;
2488            Some(map.clip_point(to, Bias::Left))
2489        } else {
2490            Some(to)
2491        }
2492    } else {
2493        None
2494    }
2495}
2496
2497fn find_backward(
2498    map: &DisplaySnapshot,
2499    from: DisplayPoint,
2500    after: bool,
2501    target: char,
2502    times: usize,
2503    mode: FindRange,
2504    smartcase: bool,
2505) -> DisplayPoint {
2506    let mut to = from;
2507
2508    for _ in 0..times {
2509        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2510            is_character_match(target, right, smartcase)
2511        });
2512        if to == new_to {
2513            break;
2514        }
2515        to = new_to;
2516    }
2517
2518    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2519    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2520        if after {
2521            *to.column_mut() += 1;
2522            map.clip_point(to, Bias::Right)
2523        } else {
2524            to
2525        }
2526    } else {
2527        from
2528    }
2529}
2530
2531fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2532    if smartcase {
2533        if target.is_uppercase() {
2534            target == other
2535        } else {
2536            target == other.to_ascii_lowercase()
2537        }
2538    } else {
2539        target == other
2540    }
2541}
2542
2543fn sneak(
2544    map: &DisplaySnapshot,
2545    from: DisplayPoint,
2546    first_target: char,
2547    second_target: char,
2548    times: usize,
2549    smartcase: bool,
2550) -> Option<DisplayPoint> {
2551    let mut to = from;
2552    let mut found = false;
2553
2554    for _ in 0..times {
2555        found = false;
2556        let new_to = find_boundary(
2557            map,
2558            movement::right(map, to),
2559            FindRange::MultiLine,
2560            |left, right| {
2561                found = is_character_match(first_target, left, smartcase)
2562                    && is_character_match(second_target, right, smartcase);
2563                found
2564            },
2565        );
2566        if to == new_to {
2567            break;
2568        }
2569        to = new_to;
2570    }
2571
2572    if found {
2573        Some(movement::left(map, to))
2574    } else {
2575        None
2576    }
2577}
2578
2579fn sneak_backward(
2580    map: &DisplaySnapshot,
2581    from: DisplayPoint,
2582    first_target: char,
2583    second_target: char,
2584    times: usize,
2585    smartcase: bool,
2586) -> Option<DisplayPoint> {
2587    let mut to = from;
2588    let mut found = false;
2589
2590    for _ in 0..times {
2591        found = false;
2592        let new_to =
2593            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2594                found = is_character_match(first_target, left, smartcase)
2595                    && is_character_match(second_target, right, smartcase);
2596                found
2597            });
2598        if to == new_to {
2599            break;
2600        }
2601        to = new_to;
2602    }
2603
2604    if found {
2605        Some(movement::left(map, to))
2606    } else {
2607        None
2608    }
2609}
2610
2611fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2612    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2613    first_non_whitespace(map, false, correct_line)
2614}
2615
2616fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2617    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2618    first_non_whitespace(map, false, correct_line)
2619}
2620
2621fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2622    let correct_line = start_of_relative_buffer_row(map, point, 0);
2623    right(map, correct_line, times.saturating_sub(1))
2624}
2625
2626pub(crate) fn next_line_end(
2627    map: &DisplaySnapshot,
2628    mut point: DisplayPoint,
2629    times: usize,
2630) -> DisplayPoint {
2631    if times > 1 {
2632        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2633    }
2634    end_of_line(map, false, point, 1)
2635}
2636
2637fn window_top(
2638    map: &DisplaySnapshot,
2639    point: DisplayPoint,
2640    text_layout_details: &TextLayoutDetails,
2641    mut times: usize,
2642) -> (DisplayPoint, SelectionGoal) {
2643    let first_visible_line = text_layout_details
2644        .scroll_anchor
2645        .anchor
2646        .to_display_point(map);
2647
2648    if first_visible_line.row() != DisplayRow(0)
2649        && text_layout_details.vertical_scroll_margin as usize > times
2650    {
2651        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2652    }
2653
2654    if let Some(visible_rows) = text_layout_details.visible_rows {
2655        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2656        let new_row = (first_visible_line.row().0 + (times as u32))
2657            .min(bottom_row)
2658            .min(map.max_point().row().0);
2659        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2660
2661        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2662        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2663    } else {
2664        let new_row =
2665            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2666        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2667
2668        let new_point = DisplayPoint::new(new_row, new_col);
2669        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2670    }
2671}
2672
2673fn window_middle(
2674    map: &DisplaySnapshot,
2675    point: DisplayPoint,
2676    text_layout_details: &TextLayoutDetails,
2677) -> (DisplayPoint, SelectionGoal) {
2678    if let Some(visible_rows) = text_layout_details.visible_rows {
2679        let first_visible_line = text_layout_details
2680            .scroll_anchor
2681            .anchor
2682            .to_display_point(map);
2683
2684        let max_visible_rows =
2685            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2686
2687        let new_row =
2688            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2689        let new_row = DisplayRow(new_row);
2690        let new_col = point.column().min(map.line_len(new_row));
2691        let new_point = DisplayPoint::new(new_row, new_col);
2692        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2693    } else {
2694        (point, SelectionGoal::None)
2695    }
2696}
2697
2698fn window_bottom(
2699    map: &DisplaySnapshot,
2700    point: DisplayPoint,
2701    text_layout_details: &TextLayoutDetails,
2702    mut times: usize,
2703) -> (DisplayPoint, SelectionGoal) {
2704    if let Some(visible_rows) = text_layout_details.visible_rows {
2705        let first_visible_line = text_layout_details
2706            .scroll_anchor
2707            .anchor
2708            .to_display_point(map);
2709        let bottom_row = first_visible_line.row().0
2710            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2711        if bottom_row < map.max_point().row().0
2712            && text_layout_details.vertical_scroll_margin as usize > times
2713        {
2714            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2715        }
2716        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2717        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2718        {
2719            first_visible_line.row()
2720        } else {
2721            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2722        };
2723        let new_col = point.column().min(map.line_len(new_row));
2724        let new_point = DisplayPoint::new(new_row, new_col);
2725        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2726    } else {
2727        (point, SelectionGoal::None)
2728    }
2729}
2730
2731fn method_motion(
2732    map: &DisplaySnapshot,
2733    mut display_point: DisplayPoint,
2734    times: usize,
2735    direction: Direction,
2736    is_start: bool,
2737) -> DisplayPoint {
2738    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2739        return display_point;
2740    };
2741
2742    for _ in 0..times {
2743        let point = map.display_point_to_point(display_point, Bias::Left);
2744        let offset = point.to_offset(&map.buffer_snapshot);
2745        let range = if direction == Direction::Prev {
2746            0..offset
2747        } else {
2748            offset..buffer.len()
2749        };
2750
2751        let possibilities = buffer
2752            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2753            .filter_map(|(range, object)| {
2754                if !matches!(object, language::TextObject::AroundFunction) {
2755                    return None;
2756                }
2757
2758                let relevant = if is_start { range.start } else { range.end };
2759                if direction == Direction::Prev && relevant < offset {
2760                    Some(relevant)
2761                } else if direction == Direction::Next && relevant > offset + 1 {
2762                    Some(relevant)
2763                } else {
2764                    None
2765                }
2766            });
2767
2768        let dest = if direction == Direction::Prev {
2769            possibilities.max().unwrap_or(offset)
2770        } else {
2771            possibilities.min().unwrap_or(offset)
2772        };
2773        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2774        if new_point == display_point {
2775            break;
2776        }
2777        display_point = new_point;
2778    }
2779    display_point
2780}
2781
2782fn comment_motion(
2783    map: &DisplaySnapshot,
2784    mut display_point: DisplayPoint,
2785    times: usize,
2786    direction: Direction,
2787) -> DisplayPoint {
2788    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2789        return display_point;
2790    };
2791
2792    for _ in 0..times {
2793        let point = map.display_point_to_point(display_point, Bias::Left);
2794        let offset = point.to_offset(&map.buffer_snapshot);
2795        let range = if direction == Direction::Prev {
2796            0..offset
2797        } else {
2798            offset..buffer.len()
2799        };
2800
2801        let possibilities = buffer
2802            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2803            .filter_map(|(range, object)| {
2804                if !matches!(object, language::TextObject::AroundComment) {
2805                    return None;
2806                }
2807
2808                let relevant = if direction == Direction::Prev {
2809                    range.start
2810                } else {
2811                    range.end
2812                };
2813                if direction == Direction::Prev && relevant < offset {
2814                    Some(relevant)
2815                } else if direction == Direction::Next && relevant > offset + 1 {
2816                    Some(relevant)
2817                } else {
2818                    None
2819                }
2820            });
2821
2822        let dest = if direction == Direction::Prev {
2823            possibilities.max().unwrap_or(offset)
2824        } else {
2825            possibilities.min().unwrap_or(offset)
2826        };
2827        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2828        if new_point == display_point {
2829            break;
2830        }
2831        display_point = new_point;
2832    }
2833
2834    display_point
2835}
2836
2837fn section_motion(
2838    map: &DisplaySnapshot,
2839    mut display_point: DisplayPoint,
2840    times: usize,
2841    direction: Direction,
2842    is_start: bool,
2843) -> DisplayPoint {
2844    if map.buffer_snapshot.as_singleton().is_some() {
2845        for _ in 0..times {
2846            let offset = map
2847                .display_point_to_point(display_point, Bias::Left)
2848                .to_offset(&map.buffer_snapshot);
2849            let range = if direction == Direction::Prev {
2850                0..offset
2851            } else {
2852                offset..map.buffer_snapshot.len()
2853            };
2854
2855            // we set a max start depth here because we want a section to only be "top level"
2856            // similar to vim's default of '{' in the first column.
2857            // (and without it, ]] at the start of editor.rs is -very- slow)
2858            let mut possibilities = map
2859                .buffer_snapshot
2860                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2861                .filter(|(_, object)| {
2862                    matches!(
2863                        object,
2864                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2865                    )
2866                })
2867                .collect::<Vec<_>>();
2868            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2869            let mut prev_end = None;
2870            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2871                if t == language::TextObject::AroundFunction
2872                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2873                {
2874                    return None;
2875                }
2876                prev_end = Some(range.end);
2877
2878                let relevant = if is_start { range.start } else { range.end };
2879                if direction == Direction::Prev && relevant < offset {
2880                    Some(relevant)
2881                } else if direction == Direction::Next && relevant > offset + 1 {
2882                    Some(relevant)
2883                } else {
2884                    None
2885                }
2886            });
2887
2888            let offset = if direction == Direction::Prev {
2889                possibilities.max().unwrap_or(0)
2890            } else {
2891                possibilities.min().unwrap_or(map.buffer_snapshot.len())
2892            };
2893
2894            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2895            if new_point == display_point {
2896                break;
2897            }
2898            display_point = new_point;
2899        }
2900        return display_point;
2901    };
2902
2903    for _ in 0..times {
2904        let next_point = if is_start {
2905            movement::start_of_excerpt(map, display_point, direction)
2906        } else {
2907            movement::end_of_excerpt(map, display_point, direction)
2908        };
2909        if next_point == display_point {
2910            break;
2911        }
2912        display_point = next_point;
2913    }
2914
2915    display_point
2916}
2917
2918fn matches_indent_type(
2919    target_indent: &text::LineIndent,
2920    current_indent: &text::LineIndent,
2921    indent_type: IndentType,
2922) -> bool {
2923    match indent_type {
2924        IndentType::Lesser => {
2925            target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
2926        }
2927        IndentType::Greater => {
2928            target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
2929        }
2930        IndentType::Same => {
2931            target_indent.spaces == current_indent.spaces
2932                && target_indent.tabs == current_indent.tabs
2933        }
2934    }
2935}
2936
2937fn indent_motion(
2938    map: &DisplaySnapshot,
2939    mut display_point: DisplayPoint,
2940    times: usize,
2941    direction: Direction,
2942    indent_type: IndentType,
2943) -> DisplayPoint {
2944    let buffer_point = map.display_point_to_point(display_point, Bias::Left);
2945    let current_row = MultiBufferRow(buffer_point.row);
2946    let current_indent = map.line_indent_for_buffer_row(current_row);
2947    if current_indent.is_line_empty() {
2948        return display_point;
2949    }
2950    let max_row = map.max_point().to_point(map).row;
2951
2952    for _ in 0..times {
2953        let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
2954
2955        let target_row = match direction {
2956            Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
2957                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
2958                !indent.is_line_empty()
2959                    && matches_indent_type(&indent, &current_indent, indent_type)
2960            }),
2961            Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
2962                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
2963                !indent.is_line_empty()
2964                    && matches_indent_type(&indent, &current_indent, indent_type)
2965            }),
2966        }
2967        .unwrap_or(current_buffer_row);
2968
2969        let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
2970        let new_point = first_non_whitespace(map, false, new_point);
2971        if new_point == display_point {
2972            break;
2973        }
2974        display_point = new_point;
2975    }
2976    display_point
2977}
2978
2979#[cfg(test)]
2980mod test {
2981
2982    use crate::{
2983        state::Mode,
2984        test::{NeovimBackedTestContext, VimTestContext},
2985    };
2986    use editor::display_map::Inlay;
2987    use indoc::indoc;
2988    use language::Point;
2989    use multi_buffer::MultiBufferRow;
2990
2991    #[gpui::test]
2992    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2993        let mut cx = NeovimBackedTestContext::new(cx).await;
2994
2995        let initial_state = indoc! {r"ˇabc
2996            def
2997
2998            paragraph
2999            the second
3000
3001
3002
3003            third and
3004            final"};
3005
3006        // goes down once
3007        cx.set_shared_state(initial_state).await;
3008        cx.simulate_shared_keystrokes("}").await;
3009        cx.shared_state().await.assert_eq(indoc! {r"abc
3010            def
3011            ˇ
3012            paragraph
3013            the second
3014
3015
3016
3017            third and
3018            final"});
3019
3020        // goes up once
3021        cx.simulate_shared_keystrokes("{").await;
3022        cx.shared_state().await.assert_eq(initial_state);
3023
3024        // goes down twice
3025        cx.simulate_shared_keystrokes("2 }").await;
3026        cx.shared_state().await.assert_eq(indoc! {r"abc
3027            def
3028
3029            paragraph
3030            the second
3031            ˇ
3032
3033
3034            third and
3035            final"});
3036
3037        // goes down over multiple blanks
3038        cx.simulate_shared_keystrokes("}").await;
3039        cx.shared_state().await.assert_eq(indoc! {r"abc
3040                def
3041
3042                paragraph
3043                the second
3044
3045
3046
3047                third and
3048                finaˇl"});
3049
3050        // goes up twice
3051        cx.simulate_shared_keystrokes("2 {").await;
3052        cx.shared_state().await.assert_eq(indoc! {r"abc
3053                def
3054                ˇ
3055                paragraph
3056                the second
3057
3058
3059
3060                third and
3061                final"});
3062    }
3063
3064    #[gpui::test]
3065    async fn test_matching(cx: &mut gpui::TestAppContext) {
3066        let mut cx = NeovimBackedTestContext::new(cx).await;
3067
3068        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3069                do(something(with<Types>.and_arrays[0, 2]))
3070            }"})
3071            .await;
3072        cx.simulate_shared_keystrokes("%").await;
3073        cx.shared_state()
3074            .await
3075            .assert_eq(indoc! {r"func (a stringˇ) {
3076                do(something(with<Types>.and_arrays[0, 2]))
3077            }"});
3078
3079        // test it works on the last character of the line
3080        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3081            do(something(with<Types>.and_arrays[0, 2]))
3082            }"})
3083            .await;
3084        cx.simulate_shared_keystrokes("%").await;
3085        cx.shared_state()
3086            .await
3087            .assert_eq(indoc! {r"func (a string) {
3088            do(something(with<Types>.and_arrays[0, 2]))
3089            ˇ}"});
3090
3091        // test it works on immediate nesting
3092        cx.set_shared_state("ˇ{()}").await;
3093        cx.simulate_shared_keystrokes("%").await;
3094        cx.shared_state().await.assert_eq("{()ˇ}");
3095        cx.simulate_shared_keystrokes("%").await;
3096        cx.shared_state().await.assert_eq("ˇ{()}");
3097
3098        // test it works on immediate nesting inside braces
3099        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3100        cx.simulate_shared_keystrokes("%").await;
3101        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3102
3103        // test it jumps to the next paren on a line
3104        cx.set_shared_state("func ˇboop() {\n}").await;
3105        cx.simulate_shared_keystrokes("%").await;
3106        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3107    }
3108
3109    #[gpui::test]
3110    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3111        let mut cx = NeovimBackedTestContext::new(cx).await;
3112
3113        // test it works with curly braces
3114        cx.set_shared_state(indoc! {r"func (a string) {
3115                do(something(with<Types>.anˇd_arrays[0, 2]))
3116            }"})
3117            .await;
3118        cx.simulate_shared_keystrokes("] }").await;
3119        cx.shared_state()
3120            .await
3121            .assert_eq(indoc! {r"func (a string) {
3122                do(something(with<Types>.and_arrays[0, 2]))
3123            ˇ}"});
3124
3125        // test it works with brackets
3126        cx.set_shared_state(indoc! {r"func (a string) {
3127                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3128            }"})
3129            .await;
3130        cx.simulate_shared_keystrokes("] )").await;
3131        cx.shared_state()
3132            .await
3133            .assert_eq(indoc! {r"func (a string) {
3134                do(something(with<Types>.and_arrays[0, 2])ˇ)
3135            }"});
3136
3137        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3138            .await;
3139        cx.simulate_shared_keystrokes("] )").await;
3140        cx.shared_state()
3141            .await
3142            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3143
3144        // test it works on immediate nesting
3145        cx.set_shared_state("{ˇ {}{}}").await;
3146        cx.simulate_shared_keystrokes("] }").await;
3147        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3148        cx.set_shared_state("(ˇ ()())").await;
3149        cx.simulate_shared_keystrokes("] )").await;
3150        cx.shared_state().await.assert_eq("( ()()ˇ)");
3151
3152        // test it works on immediate nesting inside braces
3153        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3154        cx.simulate_shared_keystrokes("] }").await;
3155        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3156        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3157        cx.simulate_shared_keystrokes("] )").await;
3158        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3159    }
3160
3161    #[gpui::test]
3162    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3163        let mut cx = NeovimBackedTestContext::new(cx).await;
3164
3165        // test it works with curly braces
3166        cx.set_shared_state(indoc! {r"func (a string) {
3167                do(something(with<Types>.anˇd_arrays[0, 2]))
3168            }"})
3169            .await;
3170        cx.simulate_shared_keystrokes("[ {").await;
3171        cx.shared_state()
3172            .await
3173            .assert_eq(indoc! {r"func (a string) ˇ{
3174                do(something(with<Types>.and_arrays[0, 2]))
3175            }"});
3176
3177        // test it works with brackets
3178        cx.set_shared_state(indoc! {r"func (a string) {
3179                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3180            }"})
3181            .await;
3182        cx.simulate_shared_keystrokes("[ (").await;
3183        cx.shared_state()
3184            .await
3185            .assert_eq(indoc! {r"func (a string) {
3186                doˇ(something(with<Types>.and_arrays[0, 2]))
3187            }"});
3188
3189        // test it works on immediate nesting
3190        cx.set_shared_state("{{}{} ˇ }").await;
3191        cx.simulate_shared_keystrokes("[ {").await;
3192        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3193        cx.set_shared_state("(()() ˇ )").await;
3194        cx.simulate_shared_keystrokes("[ (").await;
3195        cx.shared_state().await.assert_eq("ˇ(()()  )");
3196
3197        // test it works on immediate nesting inside braces
3198        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3199        cx.simulate_shared_keystrokes("[ {").await;
3200        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3201        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3202        cx.simulate_shared_keystrokes("[ (").await;
3203        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3204    }
3205
3206    #[gpui::test]
3207    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3208        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3209
3210        cx.neovim.exec("set filetype=html").await;
3211
3212        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3213        cx.simulate_shared_keystrokes("%").await;
3214        cx.shared_state()
3215            .await
3216            .assert_eq(indoc! {r"<body><ˇ/body>"});
3217        cx.simulate_shared_keystrokes("%").await;
3218
3219        // test jumping backwards
3220        cx.shared_state()
3221            .await
3222            .assert_eq(indoc! {r"<ˇbody></body>"});
3223
3224        // test self-closing tags
3225        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3226        cx.simulate_shared_keystrokes("%").await;
3227        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3228
3229        // test tag with attributes
3230        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3231            </div>
3232            "})
3233            .await;
3234        cx.simulate_shared_keystrokes("%").await;
3235        cx.shared_state()
3236            .await
3237            .assert_eq(indoc! {r"<div class='test' id='main'>
3238            <ˇ/div>
3239            "});
3240
3241        // test multi-line self-closing tag
3242        cx.set_shared_state(indoc! {r#"<a>
3243            <br
3244                test = "test"
3245            /ˇ>
3246        </a>"#})
3247            .await;
3248        cx.simulate_shared_keystrokes("%").await;
3249        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3250            ˇ<br
3251                test = "test"
3252            />
3253        </a>"#});
3254    }
3255
3256    #[gpui::test]
3257    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3258        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3259
3260        // test brackets within tags
3261        cx.set_shared_state(indoc! {r"function f() {
3262            return (
3263                <div rules={ˇ[{ a: 1 }]}>
3264                    <h1>test</h1>
3265                </div>
3266            );
3267        }"})
3268            .await;
3269        cx.simulate_shared_keystrokes("%").await;
3270        cx.shared_state().await.assert_eq(indoc! {r"function f() {
3271            return (
3272                <div rules={[{ a: 1 }ˇ]}>
3273                    <h1>test</h1>
3274                </div>
3275            );
3276        }"});
3277    }
3278
3279    #[gpui::test]
3280    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3281        let mut cx = NeovimBackedTestContext::new(cx).await;
3282
3283        // f and F
3284        cx.set_shared_state("ˇone two three four").await;
3285        cx.simulate_shared_keystrokes("f o").await;
3286        cx.shared_state().await.assert_eq("one twˇo three four");
3287        cx.simulate_shared_keystrokes(",").await;
3288        cx.shared_state().await.assert_eq("ˇone two three four");
3289        cx.simulate_shared_keystrokes("2 ;").await;
3290        cx.shared_state().await.assert_eq("one two three fˇour");
3291        cx.simulate_shared_keystrokes("shift-f e").await;
3292        cx.shared_state().await.assert_eq("one two threˇe four");
3293        cx.simulate_shared_keystrokes("2 ;").await;
3294        cx.shared_state().await.assert_eq("onˇe two three four");
3295        cx.simulate_shared_keystrokes(",").await;
3296        cx.shared_state().await.assert_eq("one two thrˇee four");
3297
3298        // t and T
3299        cx.set_shared_state("ˇone two three four").await;
3300        cx.simulate_shared_keystrokes("t o").await;
3301        cx.shared_state().await.assert_eq("one tˇwo three four");
3302        cx.simulate_shared_keystrokes(",").await;
3303        cx.shared_state().await.assert_eq("oˇne two three four");
3304        cx.simulate_shared_keystrokes("2 ;").await;
3305        cx.shared_state().await.assert_eq("one two three ˇfour");
3306        cx.simulate_shared_keystrokes("shift-t e").await;
3307        cx.shared_state().await.assert_eq("one two threeˇ four");
3308        cx.simulate_shared_keystrokes("3 ;").await;
3309        cx.shared_state().await.assert_eq("oneˇ two three four");
3310        cx.simulate_shared_keystrokes(",").await;
3311        cx.shared_state().await.assert_eq("one two thˇree four");
3312    }
3313
3314    #[gpui::test]
3315    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3316        let mut cx = NeovimBackedTestContext::new(cx).await;
3317        let initial_state = indoc! {r"something(ˇfoo)"};
3318        cx.set_shared_state(initial_state).await;
3319        cx.simulate_shared_keystrokes("}").await;
3320        cx.shared_state().await.assert_eq("something(fooˇ)");
3321    }
3322
3323    #[gpui::test]
3324    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3325        let mut cx = NeovimBackedTestContext::new(cx).await;
3326        cx.set_shared_state("ˇone\n  two\nthree").await;
3327        cx.simulate_shared_keystrokes("enter").await;
3328        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3329    }
3330
3331    #[gpui::test]
3332    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3333        let mut cx = NeovimBackedTestContext::new(cx).await;
3334        cx.set_shared_state("ˇ one\n two \nthree").await;
3335        cx.simulate_shared_keystrokes("g _").await;
3336        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3337
3338        cx.set_shared_state("ˇ one \n two \nthree").await;
3339        cx.simulate_shared_keystrokes("g _").await;
3340        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3341        cx.simulate_shared_keystrokes("2 g _").await;
3342        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3343    }
3344
3345    #[gpui::test]
3346    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3347        let mut cx = NeovimBackedTestContext::new(cx).await;
3348        let initial_state = indoc! {r"abc
3349          def
3350          paragraph
3351          the second
3352          third ˇand
3353          final"};
3354
3355        cx.set_shared_state(initial_state).await;
3356        cx.simulate_shared_keystrokes("shift-h").await;
3357        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3358          def
3359          paragraph
3360          the second
3361          third and
3362          final"});
3363
3364        // clip point
3365        cx.set_shared_state(indoc! {r"
3366          1 2 3
3367          4 5 6
3368          7 8 ˇ9
3369          "})
3370            .await;
3371        cx.simulate_shared_keystrokes("shift-h").await;
3372        cx.shared_state().await.assert_eq(indoc! {"
3373          1 2 ˇ3
3374          4 5 6
3375          7 8 9
3376          "});
3377
3378        cx.set_shared_state(indoc! {r"
3379          1 2 3
3380          4 5 6
3381          ˇ7 8 9
3382          "})
3383            .await;
3384        cx.simulate_shared_keystrokes("shift-h").await;
3385        cx.shared_state().await.assert_eq(indoc! {"
3386          ˇ1 2 3
3387          4 5 6
3388          7 8 9
3389          "});
3390
3391        cx.set_shared_state(indoc! {r"
3392          1 2 3
3393          4 5 ˇ6
3394          7 8 9"})
3395            .await;
3396        cx.simulate_shared_keystrokes("9 shift-h").await;
3397        cx.shared_state().await.assert_eq(indoc! {"
3398          1 2 3
3399          4 5 6
3400          7 8 ˇ9"});
3401    }
3402
3403    #[gpui::test]
3404    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3405        let mut cx = NeovimBackedTestContext::new(cx).await;
3406        let initial_state = indoc! {r"abˇc
3407          def
3408          paragraph
3409          the second
3410          third and
3411          final"};
3412
3413        cx.set_shared_state(initial_state).await;
3414        cx.simulate_shared_keystrokes("shift-m").await;
3415        cx.shared_state().await.assert_eq(indoc! {r"abc
3416          def
3417          paˇragraph
3418          the second
3419          third and
3420          final"});
3421
3422        cx.set_shared_state(indoc! {r"
3423          1 2 3
3424          4 5 6
3425          7 8 ˇ9
3426          "})
3427            .await;
3428        cx.simulate_shared_keystrokes("shift-m").await;
3429        cx.shared_state().await.assert_eq(indoc! {"
3430          1 2 3
3431          4 5 ˇ6
3432          7 8 9
3433          "});
3434        cx.set_shared_state(indoc! {r"
3435          1 2 3
3436          4 5 6
3437          ˇ7 8 9
3438          "})
3439            .await;
3440        cx.simulate_shared_keystrokes("shift-m").await;
3441        cx.shared_state().await.assert_eq(indoc! {"
3442          1 2 3
3443          ˇ4 5 6
3444          7 8 9
3445          "});
3446        cx.set_shared_state(indoc! {r"
3447          ˇ1 2 3
3448          4 5 6
3449          7 8 9
3450          "})
3451            .await;
3452        cx.simulate_shared_keystrokes("shift-m").await;
3453        cx.shared_state().await.assert_eq(indoc! {"
3454          1 2 3
3455          ˇ4 5 6
3456          7 8 9
3457          "});
3458        cx.set_shared_state(indoc! {r"
3459          1 2 3
3460          ˇ4 5 6
3461          7 8 9
3462          "})
3463            .await;
3464        cx.simulate_shared_keystrokes("shift-m").await;
3465        cx.shared_state().await.assert_eq(indoc! {"
3466          1 2 3
3467          ˇ4 5 6
3468          7 8 9
3469          "});
3470        cx.set_shared_state(indoc! {r"
3471          1 2 3
3472          4 5 ˇ6
3473          7 8 9
3474          "})
3475            .await;
3476        cx.simulate_shared_keystrokes("shift-m").await;
3477        cx.shared_state().await.assert_eq(indoc! {"
3478          1 2 3
3479          4 5 ˇ6
3480          7 8 9
3481          "});
3482    }
3483
3484    #[gpui::test]
3485    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3486        let mut cx = NeovimBackedTestContext::new(cx).await;
3487        let initial_state = indoc! {r"abc
3488          deˇf
3489          paragraph
3490          the second
3491          third and
3492          final"};
3493
3494        cx.set_shared_state(initial_state).await;
3495        cx.simulate_shared_keystrokes("shift-l").await;
3496        cx.shared_state().await.assert_eq(indoc! {r"abc
3497          def
3498          paragraph
3499          the second
3500          third and
3501          fiˇnal"});
3502
3503        cx.set_shared_state(indoc! {r"
3504          1 2 3
3505          4 5 ˇ6
3506          7 8 9
3507          "})
3508            .await;
3509        cx.simulate_shared_keystrokes("shift-l").await;
3510        cx.shared_state().await.assert_eq(indoc! {"
3511          1 2 3
3512          4 5 6
3513          7 8 9
3514          ˇ"});
3515
3516        cx.set_shared_state(indoc! {r"
3517          1 2 3
3518          ˇ4 5 6
3519          7 8 9
3520          "})
3521            .await;
3522        cx.simulate_shared_keystrokes("shift-l").await;
3523        cx.shared_state().await.assert_eq(indoc! {"
3524          1 2 3
3525          4 5 6
3526          7 8 9
3527          ˇ"});
3528
3529        cx.set_shared_state(indoc! {r"
3530          1 2 ˇ3
3531          4 5 6
3532          7 8 9
3533          "})
3534            .await;
3535        cx.simulate_shared_keystrokes("shift-l").await;
3536        cx.shared_state().await.assert_eq(indoc! {"
3537          1 2 3
3538          4 5 6
3539          7 8 9
3540          ˇ"});
3541
3542        cx.set_shared_state(indoc! {r"
3543          ˇ1 2 3
3544          4 5 6
3545          7 8 9
3546          "})
3547            .await;
3548        cx.simulate_shared_keystrokes("shift-l").await;
3549        cx.shared_state().await.assert_eq(indoc! {"
3550          1 2 3
3551          4 5 6
3552          7 8 9
3553          ˇ"});
3554
3555        cx.set_shared_state(indoc! {r"
3556          1 2 3
3557          4 5 ˇ6
3558          7 8 9
3559          "})
3560            .await;
3561        cx.simulate_shared_keystrokes("9 shift-l").await;
3562        cx.shared_state().await.assert_eq(indoc! {"
3563          1 2 ˇ3
3564          4 5 6
3565          7 8 9
3566          "});
3567    }
3568
3569    #[gpui::test]
3570    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3571        let mut cx = NeovimBackedTestContext::new(cx).await;
3572        cx.set_shared_state(indoc! {r"
3573        456 5ˇ67 678
3574        "})
3575            .await;
3576        cx.simulate_shared_keystrokes("g e").await;
3577        cx.shared_state().await.assert_eq(indoc! {"
3578        45ˇ6 567 678
3579        "});
3580
3581        // Test times
3582        cx.set_shared_state(indoc! {r"
3583        123 234 345
3584        456 5ˇ67 678
3585        "})
3586            .await;
3587        cx.simulate_shared_keystrokes("4 g e").await;
3588        cx.shared_state().await.assert_eq(indoc! {"
3589        12ˇ3 234 345
3590        456 567 678
3591        "});
3592
3593        // With punctuation
3594        cx.set_shared_state(indoc! {r"
3595        123 234 345
3596        4;5.6 5ˇ67 678
3597        789 890 901
3598        "})
3599            .await;
3600        cx.simulate_shared_keystrokes("g e").await;
3601        cx.shared_state().await.assert_eq(indoc! {"
3602          123 234 345
3603          4;5.ˇ6 567 678
3604          789 890 901
3605        "});
3606
3607        // With punctuation and count
3608        cx.set_shared_state(indoc! {r"
3609        123 234 345
3610        4;5.6 5ˇ67 678
3611        789 890 901
3612        "})
3613            .await;
3614        cx.simulate_shared_keystrokes("5 g e").await;
3615        cx.shared_state().await.assert_eq(indoc! {"
3616          123 234 345
3617          ˇ4;5.6 567 678
3618          789 890 901
3619        "});
3620
3621        // newlines
3622        cx.set_shared_state(indoc! {r"
3623        123 234 345
3624
3625        78ˇ9 890 901
3626        "})
3627            .await;
3628        cx.simulate_shared_keystrokes("g e").await;
3629        cx.shared_state().await.assert_eq(indoc! {"
3630          123 234 345
3631          ˇ
3632          789 890 901
3633        "});
3634        cx.simulate_shared_keystrokes("g e").await;
3635        cx.shared_state().await.assert_eq(indoc! {"
3636          123 234 34ˇ5
3637
3638          789 890 901
3639        "});
3640
3641        // With punctuation
3642        cx.set_shared_state(indoc! {r"
3643        123 234 345
3644        4;5.ˇ6 567 678
3645        789 890 901
3646        "})
3647            .await;
3648        cx.simulate_shared_keystrokes("g shift-e").await;
3649        cx.shared_state().await.assert_eq(indoc! {"
3650          123 234 34ˇ5
3651          4;5.6 567 678
3652          789 890 901
3653        "});
3654
3655        // With multi byte char
3656        cx.set_shared_state(indoc! {r"
3657        bar ˇó
3658        "})
3659            .await;
3660        cx.simulate_shared_keystrokes("g e").await;
3661        cx.shared_state().await.assert_eq(indoc! {"
3662        baˇr ó
3663        "});
3664    }
3665
3666    #[gpui::test]
3667    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3668        let mut cx = NeovimBackedTestContext::new(cx).await;
3669
3670        cx.set_shared_state(indoc! {"
3671            fn aˇ() {
3672              return
3673            }
3674        "})
3675            .await;
3676        cx.simulate_shared_keystrokes("v $ %").await;
3677        cx.shared_state().await.assert_eq(indoc! {"
3678            fn a«() {
3679              return
3680            }ˇ»
3681        "});
3682    }
3683
3684    #[gpui::test]
3685    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3686        let mut cx = VimTestContext::new(cx, true).await;
3687
3688        cx.set_state(
3689            indoc! {"
3690                struct Foo {
3691                ˇ
3692                }
3693            "},
3694            Mode::Normal,
3695        );
3696
3697        cx.update_editor(|editor, _window, cx| {
3698            let range = editor.selections.newest_anchor().range();
3699            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3700            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3701            editor.splice_inlays(&[], vec![inlay], cx);
3702        });
3703
3704        cx.simulate_keystrokes("j");
3705        cx.assert_state(
3706            indoc! {"
3707                struct Foo {
3708
3709                ˇ}
3710            "},
3711            Mode::Normal,
3712        );
3713    }
3714
3715    #[gpui::test]
3716    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3717        let mut cx = VimTestContext::new(cx, true).await;
3718
3719        cx.set_state(
3720            indoc! {"
3721            ˇstruct Foo {
3722
3723            }
3724        "},
3725            Mode::Normal,
3726        );
3727        cx.update_editor(|editor, _window, cx| {
3728            let snapshot = editor.buffer().read(cx).snapshot(cx);
3729            let end_of_line =
3730                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3731            let inlay_text = " hint";
3732            let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3733            editor.splice_inlays(&[], vec![inlay], cx);
3734        });
3735        cx.simulate_keystrokes("$");
3736        cx.assert_state(
3737            indoc! {"
3738            struct Foo ˇ{
3739
3740            }
3741        "},
3742            Mode::Normal,
3743        );
3744    }
3745
3746    #[gpui::test]
3747    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3748        let mut cx = NeovimBackedTestContext::new(cx).await;
3749        // Normal mode
3750        cx.set_shared_state(indoc! {"
3751            The ˇquick brown
3752            fox jumps over
3753            the lazy dog
3754            The quick brown
3755            fox jumps over
3756            the lazy dog
3757            The quick brown
3758            fox jumps over
3759            the lazy dog"})
3760            .await;
3761        cx.simulate_shared_keystrokes("2 0 %").await;
3762        cx.shared_state().await.assert_eq(indoc! {"
3763            The quick brown
3764            fox ˇjumps over
3765            the lazy dog
3766            The quick brown
3767            fox jumps over
3768            the lazy dog
3769            The quick brown
3770            fox jumps over
3771            the lazy dog"});
3772
3773        cx.simulate_shared_keystrokes("2 5 %").await;
3774        cx.shared_state().await.assert_eq(indoc! {"
3775            The quick brown
3776            fox jumps over
3777            the ˇlazy dog
3778            The quick brown
3779            fox jumps over
3780            the lazy dog
3781            The quick brown
3782            fox jumps over
3783            the lazy dog"});
3784
3785        cx.simulate_shared_keystrokes("7 5 %").await;
3786        cx.shared_state().await.assert_eq(indoc! {"
3787            The quick brown
3788            fox jumps over
3789            the lazy dog
3790            The quick brown
3791            fox jumps over
3792            the lazy dog
3793            The ˇquick brown
3794            fox jumps over
3795            the lazy dog"});
3796
3797        // Visual mode
3798        cx.set_shared_state(indoc! {"
3799            The ˇquick brown
3800            fox jumps over
3801            the lazy dog
3802            The quick brown
3803            fox jumps over
3804            the lazy dog
3805            The quick brown
3806            fox jumps over
3807            the lazy dog"})
3808            .await;
3809        cx.simulate_shared_keystrokes("v 5 0 %").await;
3810        cx.shared_state().await.assert_eq(indoc! {"
3811            The «quick brown
3812            fox jumps over
3813            the lazy dog
3814            The quick brown
3815            fox jˇ»umps over
3816            the lazy dog
3817            The quick brown
3818            fox jumps over
3819            the lazy dog"});
3820
3821        cx.set_shared_state(indoc! {"
3822            The ˇquick brown
3823            fox jumps over
3824            the lazy dog
3825            The quick brown
3826            fox jumps over
3827            the lazy dog
3828            The quick brown
3829            fox jumps over
3830            the lazy dog"})
3831            .await;
3832        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3833        cx.shared_state().await.assert_eq(indoc! {"
3834            The «quick brown
3835            fox jumps over
3836            the lazy dog
3837            The quick brown
3838            fox jumps over
3839            the lazy dog
3840            The quick brown
3841            fox jumps over
3842            the lˇ»azy dog"});
3843    }
3844
3845    #[gpui::test]
3846    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3847        let mut cx = NeovimBackedTestContext::new(cx).await;
3848
3849        cx.set_shared_state("ˇπππππ").await;
3850        cx.simulate_shared_keystrokes("3 space").await;
3851        cx.shared_state().await.assert_eq("πππˇππ");
3852    }
3853
3854    #[gpui::test]
3855    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3856        let mut cx = NeovimBackedTestContext::new(cx).await;
3857
3858        cx.set_shared_state(indoc! {"
3859            ππππˇπ
3860            πanotherline"})
3861            .await;
3862        cx.simulate_shared_keystrokes("4 space").await;
3863        cx.shared_state().await.assert_eq(indoc! {"
3864            πππππ
3865            πanˇotherline"});
3866    }
3867
3868    #[gpui::test]
3869    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3870        let mut cx = NeovimBackedTestContext::new(cx).await;
3871
3872        cx.set_shared_state(indoc! {"
3873                        ππππ
3874                        πanˇotherline"})
3875            .await;
3876        cx.simulate_shared_keystrokes("4 backspace").await;
3877        cx.shared_state().await.assert_eq(indoc! {"
3878                        πππˇπ
3879                        πanotherline"});
3880    }
3881
3882    #[gpui::test]
3883    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3884        let mut cx = VimTestContext::new(cx, true).await;
3885        cx.set_state(
3886            indoc! {
3887                "func empty(a string) bool {
3888                     ˇif a == \"\" {
3889                         return true
3890                     }
3891                     return false
3892                }"
3893            },
3894            Mode::Normal,
3895        );
3896        cx.simulate_keystrokes("[ -");
3897        cx.assert_state(
3898            indoc! {
3899                "ˇfunc empty(a string) bool {
3900                     if a == \"\" {
3901                         return true
3902                     }
3903                     return false
3904                }"
3905            },
3906            Mode::Normal,
3907        );
3908        cx.simulate_keystrokes("] =");
3909        cx.assert_state(
3910            indoc! {
3911                "func empty(a string) bool {
3912                     if a == \"\" {
3913                         return true
3914                     }
3915                     return false
3916                ˇ}"
3917            },
3918            Mode::Normal,
3919        );
3920        cx.simulate_keystrokes("[ +");
3921        cx.assert_state(
3922            indoc! {
3923                "func empty(a string) bool {
3924                     if a == \"\" {
3925                         return true
3926                     }
3927                     ˇreturn false
3928                }"
3929            },
3930            Mode::Normal,
3931        );
3932        cx.simulate_keystrokes("2 [ =");
3933        cx.assert_state(
3934            indoc! {
3935                "func empty(a string) bool {
3936                     ˇif a == \"\" {
3937                         return true
3938                     }
3939                     return false
3940                }"
3941            },
3942            Mode::Normal,
3943        );
3944        cx.simulate_keystrokes("] +");
3945        cx.assert_state(
3946            indoc! {
3947                "func empty(a string) bool {
3948                     if a == \"\" {
3949                         ˇreturn true
3950                     }
3951                     return false
3952                }"
3953            },
3954            Mode::Normal,
3955        );
3956        cx.simulate_keystrokes("] -");
3957        cx.assert_state(
3958            indoc! {
3959                "func empty(a string) bool {
3960                     if a == \"\" {
3961                         return true
3962                     ˇ}
3963                     return false
3964                }"
3965            },
3966            Mode::Normal,
3967        );
3968    }
3969
3970    #[gpui::test]
3971    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
3972        let mut cx = NeovimBackedTestContext::new(cx).await;
3973        cx.set_shared_state("abˇc").await;
3974        cx.simulate_shared_keystrokes("delete").await;
3975        cx.shared_state().await.assert_eq("aˇb");
3976    }
3977
3978    #[gpui::test]
3979    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
3980        let mut cx = NeovimBackedTestContext::new(cx).await;
3981
3982        cx.set_shared_state(indoc! {"
3983             ˇthe quick brown fox
3984             jumped over the lazy dog"})
3985            .await;
3986        cx.simulate_shared_keystrokes("d v 0").await;
3987        cx.shared_state().await.assert_eq(indoc! {"
3988             ˇhe quick brown fox
3989             jumped over the lazy dog"});
3990        assert_eq!(cx.cx.forced_motion(), false);
3991
3992        cx.set_shared_state(indoc! {"
3993            the quick bˇrown fox
3994            jumped over the lazy dog"})
3995            .await;
3996        cx.simulate_shared_keystrokes("d v 0").await;
3997        cx.shared_state().await.assert_eq(indoc! {"
3998            ˇown fox
3999            jumped over the lazy dog"});
4000        assert_eq!(cx.cx.forced_motion(), false);
4001
4002        cx.set_shared_state(indoc! {"
4003            the quick brown foˇx
4004            jumped over the lazy dog"})
4005            .await;
4006        cx.simulate_shared_keystrokes("d v 0").await;
4007        cx.shared_state().await.assert_eq(indoc! {"
4008            ˇ
4009            jumped over the lazy dog"});
4010        assert_eq!(cx.cx.forced_motion(), false);
4011    }
4012
4013    #[gpui::test]
4014    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4015        let mut cx = NeovimBackedTestContext::new(cx).await;
4016
4017        cx.set_shared_state(indoc! {"
4018             ˇthe quick brown fox
4019             jumped over the lazy dog"})
4020            .await;
4021        cx.simulate_shared_keystrokes("d v g shift-m").await;
4022        cx.shared_state().await.assert_eq(indoc! {"
4023             ˇbrown fox
4024             jumped over the lazy dog"});
4025        assert_eq!(cx.cx.forced_motion(), false);
4026
4027        cx.set_shared_state(indoc! {"
4028            the quick bˇrown fox
4029            jumped over the lazy dog"})
4030            .await;
4031        cx.simulate_shared_keystrokes("d v g shift-m").await;
4032        cx.shared_state().await.assert_eq(indoc! {"
4033            the quickˇown fox
4034            jumped over the lazy dog"});
4035        assert_eq!(cx.cx.forced_motion(), false);
4036
4037        cx.set_shared_state(indoc! {"
4038            the quick brown foˇx
4039            jumped over the lazy dog"})
4040            .await;
4041        cx.simulate_shared_keystrokes("d v g shift-m").await;
4042        cx.shared_state().await.assert_eq(indoc! {"
4043            the quicˇk
4044            jumped over the lazy dog"});
4045        assert_eq!(cx.cx.forced_motion(), false);
4046
4047        cx.set_shared_state(indoc! {"
4048            ˇthe quick brown fox
4049            jumped over the lazy dog"})
4050            .await;
4051        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4052        cx.shared_state().await.assert_eq(indoc! {"
4053            ˇ fox
4054            jumped over the lazy dog"});
4055        assert_eq!(cx.cx.forced_motion(), false);
4056
4057        cx.set_shared_state(indoc! {"
4058            ˇthe quick brown fox
4059            jumped over the lazy dog"})
4060            .await;
4061        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4062        cx.shared_state().await.assert_eq(indoc! {"
4063            ˇuick brown fox
4064            jumped over the lazy dog"});
4065        assert_eq!(cx.cx.forced_motion(), false);
4066    }
4067
4068    #[gpui::test]
4069    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4070        let mut cx = NeovimBackedTestContext::new(cx).await;
4071
4072        cx.set_shared_state(indoc! {"
4073             the quick brown foˇx
4074             jumped over the lazy dog"})
4075            .await;
4076        cx.simulate_shared_keystrokes("d v $").await;
4077        cx.shared_state().await.assert_eq(indoc! {"
4078             the quick brown foˇx
4079             jumped over the lazy dog"});
4080        assert_eq!(cx.cx.forced_motion(), false);
4081
4082        cx.set_shared_state(indoc! {"
4083             ˇthe quick brown fox
4084             jumped over the lazy dog"})
4085            .await;
4086        cx.simulate_shared_keystrokes("d v $").await;
4087        cx.shared_state().await.assert_eq(indoc! {"
4088             ˇx
4089             jumped over the lazy dog"});
4090        assert_eq!(cx.cx.forced_motion(), false);
4091    }
4092
4093    #[gpui::test]
4094    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4095        let mut cx = NeovimBackedTestContext::new(cx).await;
4096
4097        cx.set_shared_state(indoc! {"
4098               ˇthe quick brown fox
4099               jumped over the lazy dog"})
4100            .await;
4101        cx.simulate_shared_keystrokes("y v j p").await;
4102        cx.shared_state().await.assert_eq(indoc! {"
4103               the quick brown fox
4104               ˇthe quick brown fox
4105               jumped over the lazy dog"});
4106        assert_eq!(cx.cx.forced_motion(), false);
4107
4108        cx.set_shared_state(indoc! {"
4109              the quick bˇrown fox
4110              jumped over the lazy dog"})
4111            .await;
4112        cx.simulate_shared_keystrokes("y v j p").await;
4113        cx.shared_state().await.assert_eq(indoc! {"
4114              the quick brˇrown fox
4115              jumped overown fox
4116              jumped over the lazy dog"});
4117        assert_eq!(cx.cx.forced_motion(), false);
4118
4119        cx.set_shared_state(indoc! {"
4120             the quick brown foˇx
4121             jumped over the lazy dog"})
4122            .await;
4123        cx.simulate_shared_keystrokes("y v j p").await;
4124        cx.shared_state().await.assert_eq(indoc! {"
4125             the quick brown foxˇx
4126             jumped over the la
4127             jumped over the lazy dog"});
4128        assert_eq!(cx.cx.forced_motion(), false);
4129
4130        cx.set_shared_state(indoc! {"
4131             the quick brown fox
4132             jˇumped over the lazy dog"})
4133            .await;
4134        cx.simulate_shared_keystrokes("y v k p").await;
4135        cx.shared_state().await.assert_eq(indoc! {"
4136            thˇhe quick brown fox
4137            je quick brown fox
4138            jumped over the lazy dog"});
4139        assert_eq!(cx.cx.forced_motion(), false);
4140    }
4141
4142    #[gpui::test]
4143    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4144        let mut cx = NeovimBackedTestContext::new(cx).await;
4145
4146        cx.set_shared_state(indoc! {"
4147              ˇthe quick brown fox
4148              jumped over the lazy dog"})
4149            .await;
4150        cx.simulate_shared_keystrokes("d v e").await;
4151        cx.shared_state().await.assert_eq(indoc! {"
4152              ˇe quick brown fox
4153              jumped over the lazy dog"});
4154        assert_eq!(cx.cx.forced_motion(), false);
4155
4156        cx.set_shared_state(indoc! {"
4157              the quick bˇrown fox
4158              jumped over the lazy dog"})
4159            .await;
4160        cx.simulate_shared_keystrokes("d v e").await;
4161        cx.shared_state().await.assert_eq(indoc! {"
4162              the quick bˇn fox
4163              jumped over the lazy dog"});
4164        assert_eq!(cx.cx.forced_motion(), false);
4165
4166        cx.set_shared_state(indoc! {"
4167             the quick brown foˇx
4168             jumped over the lazy dog"})
4169            .await;
4170        cx.simulate_shared_keystrokes("d v e").await;
4171        cx.shared_state().await.assert_eq(indoc! {"
4172        the quick brown foˇd over the lazy dog"});
4173        assert_eq!(cx.cx.forced_motion(), false);
4174    }
4175}