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        point.column += 1;
1705    }
1706    for _ in 0..times {
1707        let new_point = movement::find_preceding_boundary_point(
1708            &map.buffer_snapshot,
1709            point,
1710            FindRange::MultiLine,
1711            |left, right| {
1712                let left_kind = classifier.kind(left);
1713                let right_kind = classifier.kind(right);
1714                match (left_kind, right_kind) {
1715                    (CharKind::Punctuation, CharKind::Whitespace)
1716                    | (CharKind::Punctuation, CharKind::Word)
1717                    | (CharKind::Word, CharKind::Whitespace)
1718                    | (CharKind::Word, CharKind::Punctuation) => true,
1719                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1720                    _ => false,
1721                }
1722            },
1723        );
1724        if new_point == point {
1725            break;
1726        }
1727        point = new_point;
1728    }
1729    movement::saturating_left(map, point.to_display_point(map))
1730}
1731
1732fn next_subword_start(
1733    map: &DisplaySnapshot,
1734    mut point: DisplayPoint,
1735    ignore_punctuation: bool,
1736    times: usize,
1737) -> DisplayPoint {
1738    let classifier = map
1739        .buffer_snapshot
1740        .char_classifier_at(point.to_point(map))
1741        .ignore_punctuation(ignore_punctuation);
1742    for _ in 0..times {
1743        let mut crossed_newline = false;
1744        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1745            let left_kind = classifier.kind(left);
1746            let right_kind = classifier.kind(right);
1747            let at_newline = right == '\n';
1748
1749            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1750            let is_subword_start =
1751                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1752
1753            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1754                || at_newline && crossed_newline
1755                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1756
1757            crossed_newline |= at_newline;
1758            found
1759        });
1760        if point == new_point {
1761            break;
1762        }
1763        point = new_point;
1764    }
1765    point
1766}
1767
1768pub(crate) fn next_subword_end(
1769    map: &DisplaySnapshot,
1770    mut point: DisplayPoint,
1771    ignore_punctuation: bool,
1772    times: usize,
1773    allow_cross_newline: bool,
1774) -> DisplayPoint {
1775    let classifier = map
1776        .buffer_snapshot
1777        .char_classifier_at(point.to_point(map))
1778        .ignore_punctuation(ignore_punctuation);
1779    for _ in 0..times {
1780        let new_point = next_char(map, point, allow_cross_newline);
1781
1782        let mut crossed_newline = false;
1783        let mut need_backtrack = false;
1784        let new_point =
1785            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1786                let left_kind = classifier.kind(left);
1787                let right_kind = classifier.kind(right);
1788                let at_newline = right == '\n';
1789
1790                if !allow_cross_newline && at_newline {
1791                    return true;
1792                }
1793
1794                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1795                let is_subword_end =
1796                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1797
1798                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1799
1800                if found && (is_word_end || is_subword_end) {
1801                    need_backtrack = true;
1802                }
1803
1804                crossed_newline |= at_newline;
1805                found
1806            });
1807        let mut new_point = map.clip_point(new_point, Bias::Left);
1808        if need_backtrack {
1809            *new_point.column_mut() -= 1;
1810        }
1811        let new_point = map.clip_point(new_point, Bias::Left);
1812        if point == new_point {
1813            break;
1814        }
1815        point = new_point;
1816    }
1817    point
1818}
1819
1820fn previous_subword_start(
1821    map: &DisplaySnapshot,
1822    mut point: DisplayPoint,
1823    ignore_punctuation: bool,
1824    times: usize,
1825) -> DisplayPoint {
1826    let classifier = map
1827        .buffer_snapshot
1828        .char_classifier_at(point.to_point(map))
1829        .ignore_punctuation(ignore_punctuation);
1830    for _ in 0..times {
1831        let mut crossed_newline = false;
1832        // This works even though find_preceding_boundary is called for every character in the line containing
1833        // cursor because the newline is checked only once.
1834        let new_point = movement::find_preceding_boundary_display_point(
1835            map,
1836            point,
1837            FindRange::MultiLine,
1838            |left, right| {
1839                let left_kind = classifier.kind(left);
1840                let right_kind = classifier.kind(right);
1841                let at_newline = right == '\n';
1842
1843                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1844                let is_subword_start =
1845                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1846
1847                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1848                    || at_newline && crossed_newline
1849                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1850
1851                crossed_newline |= at_newline;
1852
1853                found
1854            },
1855        );
1856        if point == new_point {
1857            break;
1858        }
1859        point = new_point;
1860    }
1861    point
1862}
1863
1864fn previous_subword_end(
1865    map: &DisplaySnapshot,
1866    point: DisplayPoint,
1867    ignore_punctuation: bool,
1868    times: usize,
1869) -> DisplayPoint {
1870    let classifier = map
1871        .buffer_snapshot
1872        .char_classifier_at(point.to_point(map))
1873        .ignore_punctuation(ignore_punctuation);
1874    let mut point = point.to_point(map);
1875
1876    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1877        point.column += 1;
1878    }
1879    for _ in 0..times {
1880        let new_point = movement::find_preceding_boundary_point(
1881            &map.buffer_snapshot,
1882            point,
1883            FindRange::MultiLine,
1884            |left, right| {
1885                let left_kind = classifier.kind(left);
1886                let right_kind = classifier.kind(right);
1887
1888                let is_subword_end =
1889                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1890
1891                if is_subword_end {
1892                    return true;
1893                }
1894
1895                match (left_kind, right_kind) {
1896                    (CharKind::Word, CharKind::Whitespace)
1897                    | (CharKind::Word, CharKind::Punctuation) => true,
1898                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1899                    _ => false,
1900                }
1901            },
1902        );
1903        if new_point == point {
1904            break;
1905        }
1906        point = new_point;
1907    }
1908    movement::saturating_left(map, point.to_display_point(map))
1909}
1910
1911pub(crate) fn first_non_whitespace(
1912    map: &DisplaySnapshot,
1913    display_lines: bool,
1914    from: DisplayPoint,
1915) -> DisplayPoint {
1916    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1917    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1918    for (ch, offset) in map.buffer_chars_at(start_offset) {
1919        if ch == '\n' {
1920            return from;
1921        }
1922
1923        start_offset = offset;
1924
1925        if classifier.kind(ch) != CharKind::Whitespace {
1926            break;
1927        }
1928    }
1929
1930    start_offset.to_display_point(map)
1931}
1932
1933pub(crate) fn last_non_whitespace(
1934    map: &DisplaySnapshot,
1935    from: DisplayPoint,
1936    count: usize,
1937) -> DisplayPoint {
1938    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1939    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1940
1941    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1942    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1943        if classifier.kind(ch) != CharKind::Whitespace {
1944            return end_of_line.to_display_point(map);
1945        }
1946    }
1947
1948    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1949        if ch == '\n' {
1950            break;
1951        }
1952        end_of_line = offset;
1953        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1954            break;
1955        }
1956    }
1957
1958    end_of_line.to_display_point(map)
1959}
1960
1961pub(crate) fn start_of_line(
1962    map: &DisplaySnapshot,
1963    display_lines: bool,
1964    point: DisplayPoint,
1965) -> DisplayPoint {
1966    if display_lines {
1967        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1968    } else {
1969        map.prev_line_boundary(point.to_point(map)).1
1970    }
1971}
1972
1973pub(crate) fn middle_of_line(
1974    map: &DisplaySnapshot,
1975    display_lines: bool,
1976    point: DisplayPoint,
1977    times: Option<usize>,
1978) -> DisplayPoint {
1979    let percent = if let Some(times) = times.filter(|&t| t <= 100) {
1980        times as f64 / 100.
1981    } else {
1982        0.5
1983    };
1984    if display_lines {
1985        map.clip_point(
1986            DisplayPoint::new(
1987                point.row(),
1988                (map.line_len(point.row()) as f64 * percent) as u32,
1989            ),
1990            Bias::Left,
1991        )
1992    } else {
1993        let mut buffer_point = point.to_point(map);
1994        buffer_point.column = (map
1995            .buffer_snapshot
1996            .line_len(MultiBufferRow(buffer_point.row)) as f64
1997            * percent) as u32;
1998
1999        map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2000    }
2001}
2002
2003pub(crate) fn end_of_line(
2004    map: &DisplaySnapshot,
2005    display_lines: bool,
2006    mut point: DisplayPoint,
2007    times: usize,
2008) -> DisplayPoint {
2009    if times > 1 {
2010        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2011    }
2012    if display_lines {
2013        map.clip_point(
2014            DisplayPoint::new(point.row(), map.line_len(point.row())),
2015            Bias::Left,
2016        )
2017    } else {
2018        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2019    }
2020}
2021
2022pub(crate) fn sentence_backwards(
2023    map: &DisplaySnapshot,
2024    point: DisplayPoint,
2025    mut times: usize,
2026) -> DisplayPoint {
2027    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2028    let mut chars = map.reverse_buffer_chars_at(start).peekable();
2029
2030    let mut was_newline = map
2031        .buffer_chars_at(start)
2032        .next()
2033        .is_some_and(|(c, _)| c == '\n');
2034
2035    while let Some((ch, offset)) = chars.next() {
2036        let start_of_next_sentence = if was_newline && ch == '\n' {
2037            Some(offset + ch.len_utf8())
2038        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2039            Some(next_non_blank(map, offset + ch.len_utf8()))
2040        } else if ch == '.' || ch == '?' || ch == '!' {
2041            start_of_next_sentence(map, offset + ch.len_utf8())
2042        } else {
2043            None
2044        };
2045
2046        if let Some(start_of_next_sentence) = start_of_next_sentence {
2047            if start_of_next_sentence < start {
2048                times = times.saturating_sub(1);
2049            }
2050            if times == 0 || offset == 0 {
2051                return map.clip_point(
2052                    start_of_next_sentence
2053                        .to_offset(&map.buffer_snapshot)
2054                        .to_display_point(map),
2055                    Bias::Left,
2056                );
2057            }
2058        }
2059        if was_newline {
2060            start = offset;
2061        }
2062        was_newline = ch == '\n';
2063    }
2064
2065    DisplayPoint::zero()
2066}
2067
2068pub(crate) fn sentence_forwards(
2069    map: &DisplaySnapshot,
2070    point: DisplayPoint,
2071    mut times: usize,
2072) -> DisplayPoint {
2073    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2074    let mut chars = map.buffer_chars_at(start).peekable();
2075
2076    let mut was_newline = map
2077        .reverse_buffer_chars_at(start)
2078        .next()
2079        .is_some_and(|(c, _)| c == '\n')
2080        && chars.peek().is_some_and(|(c, _)| *c == '\n');
2081
2082    while let Some((ch, offset)) = chars.next() {
2083        if was_newline && ch == '\n' {
2084            continue;
2085        }
2086        let start_of_next_sentence = if was_newline {
2087            Some(next_non_blank(map, offset))
2088        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2089            Some(next_non_blank(map, offset + ch.len_utf8()))
2090        } else if ch == '.' || ch == '?' || ch == '!' {
2091            start_of_next_sentence(map, offset + ch.len_utf8())
2092        } else {
2093            None
2094        };
2095
2096        if let Some(start_of_next_sentence) = start_of_next_sentence {
2097            times = times.saturating_sub(1);
2098            if times == 0 {
2099                return map.clip_point(
2100                    start_of_next_sentence
2101                        .to_offset(&map.buffer_snapshot)
2102                        .to_display_point(map),
2103                    Bias::Right,
2104                );
2105            }
2106        }
2107
2108        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2109    }
2110
2111    map.max_point()
2112}
2113
2114fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2115    for (c, o) in map.buffer_chars_at(start) {
2116        if c == '\n' || !c.is_whitespace() {
2117            return o;
2118        }
2119    }
2120
2121    map.buffer_snapshot.len()
2122}
2123
2124// given the offset after a ., !, or ? find the start of the next sentence.
2125// if this is not a sentence boundary, returns None.
2126fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2127    let chars = map.buffer_chars_at(end_of_sentence);
2128    let mut seen_space = false;
2129
2130    for (char, offset) in chars {
2131        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2132            continue;
2133        }
2134
2135        if char == '\n' && seen_space {
2136            return Some(offset);
2137        } else if char.is_whitespace() {
2138            seen_space = true;
2139        } else if seen_space {
2140            return Some(offset);
2141        } else {
2142            return None;
2143        }
2144    }
2145
2146    Some(map.buffer_snapshot.len())
2147}
2148
2149fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2150    let point = map.display_point_to_point(display_point, Bias::Left);
2151    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2152        return display_point;
2153    };
2154    let offset = excerpt.buffer().point_to_offset(
2155        excerpt
2156            .buffer()
2157            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2158    );
2159    let buffer_range = excerpt.buffer_range();
2160    if offset >= buffer_range.start && offset <= buffer_range.end {
2161        let point = map
2162            .buffer_snapshot
2163            .offset_to_point(excerpt.map_offset_from_buffer(offset));
2164        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2165    }
2166    let mut last_position = None;
2167    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2168        let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2169            ..language::ToOffset::to_offset(&range.context.end, &buffer);
2170        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2171            let text_anchor = buffer.anchor_after(offset);
2172            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2173            return anchor.to_display_point(map);
2174        } else if offset <= excerpt_range.start {
2175            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2176            return anchor.to_display_point(map);
2177        } else {
2178            last_position = Some(Anchor::in_buffer(
2179                excerpt,
2180                buffer.remote_id(),
2181                range.context.end,
2182            ));
2183        }
2184    }
2185
2186    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2187    last_point.column = point.column;
2188
2189    map.clip_point(
2190        map.point_to_display_point(
2191            map.buffer_snapshot.clip_point(point, Bias::Left),
2192            Bias::Left,
2193        ),
2194        Bias::Left,
2195    )
2196}
2197
2198fn start_of_document(
2199    map: &DisplaySnapshot,
2200    display_point: DisplayPoint,
2201    maybe_times: Option<usize>,
2202) -> DisplayPoint {
2203    if let Some(times) = maybe_times {
2204        return go_to_line(map, display_point, times);
2205    }
2206
2207    let point = map.display_point_to_point(display_point, Bias::Left);
2208    let mut first_point = Point::zero();
2209    first_point.column = point.column;
2210
2211    map.clip_point(
2212        map.point_to_display_point(
2213            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2214            Bias::Left,
2215        ),
2216        Bias::Left,
2217    )
2218}
2219
2220fn end_of_document(
2221    map: &DisplaySnapshot,
2222    display_point: DisplayPoint,
2223    maybe_times: Option<usize>,
2224) -> DisplayPoint {
2225    if let Some(times) = maybe_times {
2226        return go_to_line(map, display_point, times);
2227    };
2228    let point = map.display_point_to_point(display_point, Bias::Left);
2229    let mut last_point = map.buffer_snapshot.max_point();
2230    last_point.column = point.column;
2231
2232    map.clip_point(
2233        map.point_to_display_point(
2234            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2235            Bias::Left,
2236        ),
2237        Bias::Left,
2238    )
2239}
2240
2241fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2242    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2243    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2244
2245    if head > outer.start && head < inner.start {
2246        let mut offset = inner.end.to_offset(map, Bias::Left);
2247        for c in map.buffer_snapshot.chars_at(offset) {
2248            if c == '/' || c == '\n' || c == '>' {
2249                return Some(offset.to_display_point(map));
2250            }
2251            offset += c.len_utf8();
2252        }
2253    } else {
2254        let mut offset = outer.start.to_offset(map, Bias::Left);
2255        for c in map.buffer_snapshot.chars_at(offset) {
2256            offset += c.len_utf8();
2257            if c == '<' || c == '\n' {
2258                return Some(offset.to_display_point(map));
2259            }
2260        }
2261    }
2262
2263    return None;
2264}
2265
2266fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2267    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2268    let display_point = map.clip_at_line_end(display_point);
2269    let point = display_point.to_point(map);
2270    let offset = point.to_offset(&map.buffer_snapshot);
2271
2272    // Ensure the range is contained by the current line.
2273    let mut line_end = map.next_line_boundary(point).0;
2274    if line_end == point {
2275        line_end = map.max_point().to_point(map);
2276    }
2277
2278    let line_range = map.prev_line_boundary(point).0..line_end;
2279    let visible_line_range =
2280        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2281    let ranges = map
2282        .buffer_snapshot
2283        .bracket_ranges(visible_line_range.clone());
2284    if let Some(ranges) = ranges {
2285        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2286            ..line_range.end.to_offset(&map.buffer_snapshot);
2287        let mut closest_pair_destination = None;
2288        let mut closest_distance = usize::MAX;
2289
2290        for (open_range, close_range) in ranges {
2291            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2292                if offset > open_range.start && offset < close_range.start {
2293                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2294                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2295                        return display_point;
2296                    }
2297                    if let Some(tag) = matching_tag(map, display_point) {
2298                        return tag;
2299                    }
2300                } else if close_range.contains(&offset) {
2301                    return open_range.start.to_display_point(map);
2302                } else if open_range.contains(&offset) {
2303                    return (close_range.end - 1).to_display_point(map);
2304                }
2305            }
2306
2307            if (open_range.contains(&offset) || open_range.start >= offset)
2308                && line_range.contains(&open_range.start)
2309            {
2310                let distance = open_range.start.saturating_sub(offset);
2311                if distance < closest_distance {
2312                    closest_pair_destination = Some(close_range.start);
2313                    closest_distance = distance;
2314                    continue;
2315                }
2316            }
2317
2318            if (close_range.contains(&offset) || close_range.start >= offset)
2319                && line_range.contains(&close_range.start)
2320            {
2321                let distance = close_range.start.saturating_sub(offset);
2322                if distance < closest_distance {
2323                    closest_pair_destination = Some(open_range.start);
2324                    closest_distance = distance;
2325                    continue;
2326                }
2327            }
2328
2329            continue;
2330        }
2331
2332        closest_pair_destination
2333            .map(|destination| destination.to_display_point(map))
2334            .unwrap_or(display_point)
2335    } else {
2336        display_point
2337    }
2338}
2339
2340// Go to {count} percentage in the file, on the first
2341// non-blank in the line linewise.  To compute the new
2342// line number this formula is used:
2343// ({count} * number-of-lines + 99) / 100
2344//
2345// https://neovim.io/doc/user/motion.html#N%25
2346fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2347    let total_lines = map.buffer_snapshot.max_point().row + 1;
2348    let target_line = (count * total_lines as usize).div_ceil(100);
2349    let target_point = DisplayPoint::new(
2350        DisplayRow(target_line.saturating_sub(1) as u32),
2351        point.column(),
2352    );
2353    map.clip_point(target_point, Bias::Left)
2354}
2355
2356fn unmatched_forward(
2357    map: &DisplaySnapshot,
2358    mut display_point: DisplayPoint,
2359    char: char,
2360    times: usize,
2361) -> DisplayPoint {
2362    for _ in 0..times {
2363        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2364        let point = display_point.to_point(map);
2365        let offset = point.to_offset(&map.buffer_snapshot);
2366
2367        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2368        let Some(ranges) = ranges else { break };
2369        let mut closest_closing_destination = None;
2370        let mut closest_distance = usize::MAX;
2371
2372        for (_, close_range) in ranges {
2373            if close_range.start > offset {
2374                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2375                if Some(char) == chars.next() {
2376                    let distance = close_range.start - offset;
2377                    if distance < closest_distance {
2378                        closest_closing_destination = Some(close_range.start);
2379                        closest_distance = distance;
2380                        continue;
2381                    }
2382                }
2383            }
2384        }
2385
2386        let new_point = closest_closing_destination
2387            .map(|destination| destination.to_display_point(map))
2388            .unwrap_or(display_point);
2389        if new_point == display_point {
2390            break;
2391        }
2392        display_point = new_point;
2393    }
2394    return display_point;
2395}
2396
2397fn unmatched_backward(
2398    map: &DisplaySnapshot,
2399    mut display_point: DisplayPoint,
2400    char: char,
2401    times: usize,
2402) -> DisplayPoint {
2403    for _ in 0..times {
2404        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2405        let point = display_point.to_point(map);
2406        let offset = point.to_offset(&map.buffer_snapshot);
2407
2408        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2409        let Some(ranges) = ranges else {
2410            break;
2411        };
2412
2413        let mut closest_starting_destination = None;
2414        let mut closest_distance = usize::MAX;
2415
2416        for (start_range, _) in ranges {
2417            if start_range.start < offset {
2418                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2419                if Some(char) == chars.next() {
2420                    let distance = offset - start_range.start;
2421                    if distance < closest_distance {
2422                        closest_starting_destination = Some(start_range.start);
2423                        closest_distance = distance;
2424                        continue;
2425                    }
2426                }
2427            }
2428        }
2429
2430        let new_point = closest_starting_destination
2431            .map(|destination| destination.to_display_point(map))
2432            .unwrap_or(display_point);
2433        if new_point == display_point {
2434            break;
2435        } else {
2436            display_point = new_point;
2437        }
2438    }
2439    display_point
2440}
2441
2442fn find_forward(
2443    map: &DisplaySnapshot,
2444    from: DisplayPoint,
2445    before: bool,
2446    target: char,
2447    times: usize,
2448    mode: FindRange,
2449    smartcase: bool,
2450) -> Option<DisplayPoint> {
2451    let mut to = from;
2452    let mut found = false;
2453
2454    for _ in 0..times {
2455        found = false;
2456        let new_to = find_boundary(map, to, mode, |_, right| {
2457            found = is_character_match(target, right, smartcase);
2458            found
2459        });
2460        if to == new_to {
2461            break;
2462        }
2463        to = new_to;
2464    }
2465
2466    if found {
2467        if before && to.column() > 0 {
2468            *to.column_mut() -= 1;
2469            Some(map.clip_point(to, Bias::Left))
2470        } else if before && to.row().0 > 0 {
2471            *to.row_mut() -= 1;
2472            *to.column_mut() = map.line(to.row()).len() as u32;
2473            Some(map.clip_point(to, Bias::Left))
2474        } else {
2475            Some(to)
2476        }
2477    } else {
2478        None
2479    }
2480}
2481
2482fn find_backward(
2483    map: &DisplaySnapshot,
2484    from: DisplayPoint,
2485    after: bool,
2486    target: char,
2487    times: usize,
2488    mode: FindRange,
2489    smartcase: bool,
2490) -> DisplayPoint {
2491    let mut to = from;
2492
2493    for _ in 0..times {
2494        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2495            is_character_match(target, right, smartcase)
2496        });
2497        if to == new_to {
2498            break;
2499        }
2500        to = new_to;
2501    }
2502
2503    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2504    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2505        if after {
2506            *to.column_mut() += 1;
2507            map.clip_point(to, Bias::Right)
2508        } else {
2509            to
2510        }
2511    } else {
2512        from
2513    }
2514}
2515
2516fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2517    if smartcase {
2518        if target.is_uppercase() {
2519            target == other
2520        } else {
2521            target == other.to_ascii_lowercase()
2522        }
2523    } else {
2524        target == other
2525    }
2526}
2527
2528fn sneak(
2529    map: &DisplaySnapshot,
2530    from: DisplayPoint,
2531    first_target: char,
2532    second_target: char,
2533    times: usize,
2534    smartcase: bool,
2535) -> Option<DisplayPoint> {
2536    let mut to = from;
2537    let mut found = false;
2538
2539    for _ in 0..times {
2540        found = false;
2541        let new_to = find_boundary(
2542            map,
2543            movement::right(map, to),
2544            FindRange::MultiLine,
2545            |left, right| {
2546                found = is_character_match(first_target, left, smartcase)
2547                    && is_character_match(second_target, right, smartcase);
2548                found
2549            },
2550        );
2551        if to == new_to {
2552            break;
2553        }
2554        to = new_to;
2555    }
2556
2557    if found {
2558        Some(movement::left(map, to))
2559    } else {
2560        None
2561    }
2562}
2563
2564fn sneak_backward(
2565    map: &DisplaySnapshot,
2566    from: DisplayPoint,
2567    first_target: char,
2568    second_target: char,
2569    times: usize,
2570    smartcase: bool,
2571) -> Option<DisplayPoint> {
2572    let mut to = from;
2573    let mut found = false;
2574
2575    for _ in 0..times {
2576        found = false;
2577        let new_to =
2578            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2579                found = is_character_match(first_target, left, smartcase)
2580                    && is_character_match(second_target, right, smartcase);
2581                found
2582            });
2583        if to == new_to {
2584            break;
2585        }
2586        to = new_to;
2587    }
2588
2589    if found {
2590        Some(movement::left(map, to))
2591    } else {
2592        None
2593    }
2594}
2595
2596fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2597    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2598    first_non_whitespace(map, false, correct_line)
2599}
2600
2601fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2602    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2603    first_non_whitespace(map, false, correct_line)
2604}
2605
2606fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2607    let correct_line = start_of_relative_buffer_row(map, point, 0);
2608    right(map, correct_line, times.saturating_sub(1))
2609}
2610
2611pub(crate) fn next_line_end(
2612    map: &DisplaySnapshot,
2613    mut point: DisplayPoint,
2614    times: usize,
2615) -> DisplayPoint {
2616    if times > 1 {
2617        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2618    }
2619    end_of_line(map, false, point, 1)
2620}
2621
2622fn window_top(
2623    map: &DisplaySnapshot,
2624    point: DisplayPoint,
2625    text_layout_details: &TextLayoutDetails,
2626    mut times: usize,
2627) -> (DisplayPoint, SelectionGoal) {
2628    let first_visible_line = text_layout_details
2629        .scroll_anchor
2630        .anchor
2631        .to_display_point(map);
2632
2633    if first_visible_line.row() != DisplayRow(0)
2634        && text_layout_details.vertical_scroll_margin as usize > times
2635    {
2636        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2637    }
2638
2639    if let Some(visible_rows) = text_layout_details.visible_rows {
2640        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2641        let new_row = (first_visible_line.row().0 + (times as u32))
2642            .min(bottom_row)
2643            .min(map.max_point().row().0);
2644        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2645
2646        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2647        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2648    } else {
2649        let new_row =
2650            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2651        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2652
2653        let new_point = DisplayPoint::new(new_row, new_col);
2654        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2655    }
2656}
2657
2658fn window_middle(
2659    map: &DisplaySnapshot,
2660    point: DisplayPoint,
2661    text_layout_details: &TextLayoutDetails,
2662) -> (DisplayPoint, SelectionGoal) {
2663    if let Some(visible_rows) = text_layout_details.visible_rows {
2664        let first_visible_line = text_layout_details
2665            .scroll_anchor
2666            .anchor
2667            .to_display_point(map);
2668
2669        let max_visible_rows =
2670            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2671
2672        let new_row =
2673            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2674        let new_row = DisplayRow(new_row);
2675        let new_col = point.column().min(map.line_len(new_row));
2676        let new_point = DisplayPoint::new(new_row, new_col);
2677        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2678    } else {
2679        (point, SelectionGoal::None)
2680    }
2681}
2682
2683fn window_bottom(
2684    map: &DisplaySnapshot,
2685    point: DisplayPoint,
2686    text_layout_details: &TextLayoutDetails,
2687    mut times: usize,
2688) -> (DisplayPoint, SelectionGoal) {
2689    if let Some(visible_rows) = text_layout_details.visible_rows {
2690        let first_visible_line = text_layout_details
2691            .scroll_anchor
2692            .anchor
2693            .to_display_point(map);
2694        let bottom_row = first_visible_line.row().0
2695            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2696        if bottom_row < map.max_point().row().0
2697            && text_layout_details.vertical_scroll_margin as usize > times
2698        {
2699            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2700        }
2701        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2702        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2703        {
2704            first_visible_line.row()
2705        } else {
2706            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2707        };
2708        let new_col = point.column().min(map.line_len(new_row));
2709        let new_point = DisplayPoint::new(new_row, new_col);
2710        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2711    } else {
2712        (point, SelectionGoal::None)
2713    }
2714}
2715
2716fn method_motion(
2717    map: &DisplaySnapshot,
2718    mut display_point: DisplayPoint,
2719    times: usize,
2720    direction: Direction,
2721    is_start: bool,
2722) -> DisplayPoint {
2723    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2724        return display_point;
2725    };
2726
2727    for _ in 0..times {
2728        let point = map.display_point_to_point(display_point, Bias::Left);
2729        let offset = point.to_offset(&map.buffer_snapshot);
2730        let range = if direction == Direction::Prev {
2731            0..offset
2732        } else {
2733            offset..buffer.len()
2734        };
2735
2736        let possibilities = buffer
2737            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2738            .filter_map(|(range, object)| {
2739                if !matches!(object, language::TextObject::AroundFunction) {
2740                    return None;
2741                }
2742
2743                let relevant = if is_start { range.start } else { range.end };
2744                if direction == Direction::Prev && relevant < offset {
2745                    Some(relevant)
2746                } else if direction == Direction::Next && relevant > offset + 1 {
2747                    Some(relevant)
2748                } else {
2749                    None
2750                }
2751            });
2752
2753        let dest = if direction == Direction::Prev {
2754            possibilities.max().unwrap_or(offset)
2755        } else {
2756            possibilities.min().unwrap_or(offset)
2757        };
2758        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2759        if new_point == display_point {
2760            break;
2761        }
2762        display_point = new_point;
2763    }
2764    display_point
2765}
2766
2767fn comment_motion(
2768    map: &DisplaySnapshot,
2769    mut display_point: DisplayPoint,
2770    times: usize,
2771    direction: Direction,
2772) -> DisplayPoint {
2773    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2774        return display_point;
2775    };
2776
2777    for _ in 0..times {
2778        let point = map.display_point_to_point(display_point, Bias::Left);
2779        let offset = point.to_offset(&map.buffer_snapshot);
2780        let range = if direction == Direction::Prev {
2781            0..offset
2782        } else {
2783            offset..buffer.len()
2784        };
2785
2786        let possibilities = buffer
2787            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2788            .filter_map(|(range, object)| {
2789                if !matches!(object, language::TextObject::AroundComment) {
2790                    return None;
2791                }
2792
2793                let relevant = if direction == Direction::Prev {
2794                    range.start
2795                } else {
2796                    range.end
2797                };
2798                if direction == Direction::Prev && relevant < offset {
2799                    Some(relevant)
2800                } else if direction == Direction::Next && relevant > offset + 1 {
2801                    Some(relevant)
2802                } else {
2803                    None
2804                }
2805            });
2806
2807        let dest = if direction == Direction::Prev {
2808            possibilities.max().unwrap_or(offset)
2809        } else {
2810            possibilities.min().unwrap_or(offset)
2811        };
2812        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2813        if new_point == display_point {
2814            break;
2815        }
2816        display_point = new_point;
2817    }
2818
2819    display_point
2820}
2821
2822fn section_motion(
2823    map: &DisplaySnapshot,
2824    mut display_point: DisplayPoint,
2825    times: usize,
2826    direction: Direction,
2827    is_start: bool,
2828) -> DisplayPoint {
2829    if map.buffer_snapshot.as_singleton().is_some() {
2830        for _ in 0..times {
2831            let offset = map
2832                .display_point_to_point(display_point, Bias::Left)
2833                .to_offset(&map.buffer_snapshot);
2834            let range = if direction == Direction::Prev {
2835                0..offset
2836            } else {
2837                offset..map.buffer_snapshot.len()
2838            };
2839
2840            // we set a max start depth here because we want a section to only be "top level"
2841            // similar to vim's default of '{' in the first column.
2842            // (and without it, ]] at the start of editor.rs is -very- slow)
2843            let mut possibilities = map
2844                .buffer_snapshot
2845                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2846                .filter(|(_, object)| {
2847                    matches!(
2848                        object,
2849                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2850                    )
2851                })
2852                .collect::<Vec<_>>();
2853            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2854            let mut prev_end = None;
2855            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2856                if t == language::TextObject::AroundFunction
2857                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2858                {
2859                    return None;
2860                }
2861                prev_end = Some(range.end);
2862
2863                let relevant = if is_start { range.start } else { range.end };
2864                if direction == Direction::Prev && relevant < offset {
2865                    Some(relevant)
2866                } else if direction == Direction::Next && relevant > offset + 1 {
2867                    Some(relevant)
2868                } else {
2869                    None
2870                }
2871            });
2872
2873            let offset = if direction == Direction::Prev {
2874                possibilities.max().unwrap_or(0)
2875            } else {
2876                possibilities.min().unwrap_or(map.buffer_snapshot.len())
2877            };
2878
2879            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2880            if new_point == display_point {
2881                break;
2882            }
2883            display_point = new_point;
2884        }
2885        return display_point;
2886    };
2887
2888    for _ in 0..times {
2889        let next_point = if is_start {
2890            movement::start_of_excerpt(map, display_point, direction)
2891        } else {
2892            movement::end_of_excerpt(map, display_point, direction)
2893        };
2894        if next_point == display_point {
2895            break;
2896        }
2897        display_point = next_point;
2898    }
2899
2900    display_point
2901}
2902
2903fn matches_indent_type(
2904    target_indent: &text::LineIndent,
2905    current_indent: &text::LineIndent,
2906    indent_type: IndentType,
2907) -> bool {
2908    match indent_type {
2909        IndentType::Lesser => {
2910            target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
2911        }
2912        IndentType::Greater => {
2913            target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
2914        }
2915        IndentType::Same => {
2916            target_indent.spaces == current_indent.spaces
2917                && target_indent.tabs == current_indent.tabs
2918        }
2919    }
2920}
2921
2922fn indent_motion(
2923    map: &DisplaySnapshot,
2924    mut display_point: DisplayPoint,
2925    times: usize,
2926    direction: Direction,
2927    indent_type: IndentType,
2928) -> DisplayPoint {
2929    let buffer_point = map.display_point_to_point(display_point, Bias::Left);
2930    let current_row = MultiBufferRow(buffer_point.row);
2931    let current_indent = map.line_indent_for_buffer_row(current_row);
2932    if current_indent.is_line_empty() {
2933        return display_point;
2934    }
2935    let max_row = map.max_point().to_point(map).row;
2936
2937    for _ in 0..times {
2938        let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
2939
2940        let target_row = match direction {
2941            Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
2942                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
2943                !indent.is_line_empty()
2944                    && matches_indent_type(&indent, &current_indent, indent_type)
2945            }),
2946            Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
2947                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
2948                !indent.is_line_empty()
2949                    && matches_indent_type(&indent, &current_indent, indent_type)
2950            }),
2951        }
2952        .unwrap_or(current_buffer_row);
2953
2954        let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
2955        let new_point = first_non_whitespace(map, false, new_point);
2956        if new_point == display_point {
2957            break;
2958        }
2959        display_point = new_point;
2960    }
2961    display_point
2962}
2963
2964#[cfg(test)]
2965mod test {
2966
2967    use crate::{
2968        state::Mode,
2969        test::{NeovimBackedTestContext, VimTestContext},
2970    };
2971    use editor::display_map::Inlay;
2972    use indoc::indoc;
2973    use language::Point;
2974    use multi_buffer::MultiBufferRow;
2975
2976    #[gpui::test]
2977    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2978        let mut cx = NeovimBackedTestContext::new(cx).await;
2979
2980        let initial_state = indoc! {r"ˇabc
2981            def
2982
2983            paragraph
2984            the second
2985
2986
2987
2988            third and
2989            final"};
2990
2991        // goes down once
2992        cx.set_shared_state(initial_state).await;
2993        cx.simulate_shared_keystrokes("}").await;
2994        cx.shared_state().await.assert_eq(indoc! {r"abc
2995            def
2996            ˇ
2997            paragraph
2998            the second
2999
3000
3001
3002            third and
3003            final"});
3004
3005        // goes up once
3006        cx.simulate_shared_keystrokes("{").await;
3007        cx.shared_state().await.assert_eq(initial_state);
3008
3009        // goes down twice
3010        cx.simulate_shared_keystrokes("2 }").await;
3011        cx.shared_state().await.assert_eq(indoc! {r"abc
3012            def
3013
3014            paragraph
3015            the second
3016            ˇ
3017
3018
3019            third and
3020            final"});
3021
3022        // goes down over multiple blanks
3023        cx.simulate_shared_keystrokes("}").await;
3024        cx.shared_state().await.assert_eq(indoc! {r"abc
3025                def
3026
3027                paragraph
3028                the second
3029
3030
3031
3032                third and
3033                finaˇl"});
3034
3035        // goes up twice
3036        cx.simulate_shared_keystrokes("2 {").await;
3037        cx.shared_state().await.assert_eq(indoc! {r"abc
3038                def
3039                ˇ
3040                paragraph
3041                the second
3042
3043
3044
3045                third and
3046                final"});
3047    }
3048
3049    #[gpui::test]
3050    async fn test_matching(cx: &mut gpui::TestAppContext) {
3051        let mut cx = NeovimBackedTestContext::new(cx).await;
3052
3053        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3054                do(something(with<Types>.and_arrays[0, 2]))
3055            }"})
3056            .await;
3057        cx.simulate_shared_keystrokes("%").await;
3058        cx.shared_state()
3059            .await
3060            .assert_eq(indoc! {r"func (a stringˇ) {
3061                do(something(with<Types>.and_arrays[0, 2]))
3062            }"});
3063
3064        // test it works on the last character of the line
3065        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3066            do(something(with<Types>.and_arrays[0, 2]))
3067            }"})
3068            .await;
3069        cx.simulate_shared_keystrokes("%").await;
3070        cx.shared_state()
3071            .await
3072            .assert_eq(indoc! {r"func (a string) {
3073            do(something(with<Types>.and_arrays[0, 2]))
3074            ˇ}"});
3075
3076        // test it works on immediate nesting
3077        cx.set_shared_state("ˇ{()}").await;
3078        cx.simulate_shared_keystrokes("%").await;
3079        cx.shared_state().await.assert_eq("{()ˇ}");
3080        cx.simulate_shared_keystrokes("%").await;
3081        cx.shared_state().await.assert_eq("ˇ{()}");
3082
3083        // test it works on immediate nesting inside braces
3084        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3085        cx.simulate_shared_keystrokes("%").await;
3086        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3087
3088        // test it jumps to the next paren on a line
3089        cx.set_shared_state("func ˇboop() {\n}").await;
3090        cx.simulate_shared_keystrokes("%").await;
3091        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3092    }
3093
3094    #[gpui::test]
3095    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3096        let mut cx = NeovimBackedTestContext::new(cx).await;
3097
3098        // test it works with curly braces
3099        cx.set_shared_state(indoc! {r"func (a string) {
3100                do(something(with<Types>.anˇd_arrays[0, 2]))
3101            }"})
3102            .await;
3103        cx.simulate_shared_keystrokes("] }").await;
3104        cx.shared_state()
3105            .await
3106            .assert_eq(indoc! {r"func (a string) {
3107                do(something(with<Types>.and_arrays[0, 2]))
3108            ˇ}"});
3109
3110        // test it works with brackets
3111        cx.set_shared_state(indoc! {r"func (a string) {
3112                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3113            }"})
3114            .await;
3115        cx.simulate_shared_keystrokes("] )").await;
3116        cx.shared_state()
3117            .await
3118            .assert_eq(indoc! {r"func (a string) {
3119                do(something(with<Types>.and_arrays[0, 2])ˇ)
3120            }"});
3121
3122        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3123            .await;
3124        cx.simulate_shared_keystrokes("] )").await;
3125        cx.shared_state()
3126            .await
3127            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3128
3129        // test it works on immediate nesting
3130        cx.set_shared_state("{ˇ {}{}}").await;
3131        cx.simulate_shared_keystrokes("] }").await;
3132        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3133        cx.set_shared_state("(ˇ ()())").await;
3134        cx.simulate_shared_keystrokes("] )").await;
3135        cx.shared_state().await.assert_eq("( ()()ˇ)");
3136
3137        // test it works on immediate nesting inside braces
3138        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3139        cx.simulate_shared_keystrokes("] }").await;
3140        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3141        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3142        cx.simulate_shared_keystrokes("] )").await;
3143        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3144    }
3145
3146    #[gpui::test]
3147    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3148        let mut cx = NeovimBackedTestContext::new(cx).await;
3149
3150        // test it works with curly braces
3151        cx.set_shared_state(indoc! {r"func (a string) {
3152                do(something(with<Types>.anˇd_arrays[0, 2]))
3153            }"})
3154            .await;
3155        cx.simulate_shared_keystrokes("[ {").await;
3156        cx.shared_state()
3157            .await
3158            .assert_eq(indoc! {r"func (a string) ˇ{
3159                do(something(with<Types>.and_arrays[0, 2]))
3160            }"});
3161
3162        // test it works with brackets
3163        cx.set_shared_state(indoc! {r"func (a string) {
3164                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3165            }"})
3166            .await;
3167        cx.simulate_shared_keystrokes("[ (").await;
3168        cx.shared_state()
3169            .await
3170            .assert_eq(indoc! {r"func (a string) {
3171                doˇ(something(with<Types>.and_arrays[0, 2]))
3172            }"});
3173
3174        // test it works on immediate nesting
3175        cx.set_shared_state("{{}{} ˇ }").await;
3176        cx.simulate_shared_keystrokes("[ {").await;
3177        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3178        cx.set_shared_state("(()() ˇ )").await;
3179        cx.simulate_shared_keystrokes("[ (").await;
3180        cx.shared_state().await.assert_eq("ˇ(()()  )");
3181
3182        // test it works on immediate nesting inside braces
3183        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3184        cx.simulate_shared_keystrokes("[ {").await;
3185        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3186        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3187        cx.simulate_shared_keystrokes("[ (").await;
3188        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3189    }
3190
3191    #[gpui::test]
3192    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3193        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3194
3195        cx.neovim.exec("set filetype=html").await;
3196
3197        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3198        cx.simulate_shared_keystrokes("%").await;
3199        cx.shared_state()
3200            .await
3201            .assert_eq(indoc! {r"<body><ˇ/body>"});
3202        cx.simulate_shared_keystrokes("%").await;
3203
3204        // test jumping backwards
3205        cx.shared_state()
3206            .await
3207            .assert_eq(indoc! {r"<ˇbody></body>"});
3208
3209        // test self-closing tags
3210        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3211        cx.simulate_shared_keystrokes("%").await;
3212        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3213
3214        // test tag with attributes
3215        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3216            </div>
3217            "})
3218            .await;
3219        cx.simulate_shared_keystrokes("%").await;
3220        cx.shared_state()
3221            .await
3222            .assert_eq(indoc! {r"<div class='test' id='main'>
3223            <ˇ/div>
3224            "});
3225
3226        // test multi-line self-closing tag
3227        cx.set_shared_state(indoc! {r#"<a>
3228            <br
3229                test = "test"
3230            /ˇ>
3231        </a>"#})
3232            .await;
3233        cx.simulate_shared_keystrokes("%").await;
3234        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3235            ˇ<br
3236                test = "test"
3237            />
3238        </a>"#});
3239    }
3240
3241    #[gpui::test]
3242    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3243        let mut cx = NeovimBackedTestContext::new(cx).await;
3244
3245        // f and F
3246        cx.set_shared_state("ˇone two three four").await;
3247        cx.simulate_shared_keystrokes("f o").await;
3248        cx.shared_state().await.assert_eq("one twˇo three four");
3249        cx.simulate_shared_keystrokes(",").await;
3250        cx.shared_state().await.assert_eq("ˇone two three four");
3251        cx.simulate_shared_keystrokes("2 ;").await;
3252        cx.shared_state().await.assert_eq("one two three fˇour");
3253        cx.simulate_shared_keystrokes("shift-f e").await;
3254        cx.shared_state().await.assert_eq("one two threˇe four");
3255        cx.simulate_shared_keystrokes("2 ;").await;
3256        cx.shared_state().await.assert_eq("onˇe two three four");
3257        cx.simulate_shared_keystrokes(",").await;
3258        cx.shared_state().await.assert_eq("one two thrˇee four");
3259
3260        // t and T
3261        cx.set_shared_state("ˇone two three four").await;
3262        cx.simulate_shared_keystrokes("t o").await;
3263        cx.shared_state().await.assert_eq("one tˇwo three four");
3264        cx.simulate_shared_keystrokes(",").await;
3265        cx.shared_state().await.assert_eq("oˇne two three four");
3266        cx.simulate_shared_keystrokes("2 ;").await;
3267        cx.shared_state().await.assert_eq("one two three ˇfour");
3268        cx.simulate_shared_keystrokes("shift-t e").await;
3269        cx.shared_state().await.assert_eq("one two threeˇ four");
3270        cx.simulate_shared_keystrokes("3 ;").await;
3271        cx.shared_state().await.assert_eq("oneˇ two three four");
3272        cx.simulate_shared_keystrokes(",").await;
3273        cx.shared_state().await.assert_eq("one two thˇree four");
3274    }
3275
3276    #[gpui::test]
3277    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3278        let mut cx = NeovimBackedTestContext::new(cx).await;
3279        let initial_state = indoc! {r"something(ˇfoo)"};
3280        cx.set_shared_state(initial_state).await;
3281        cx.simulate_shared_keystrokes("}").await;
3282        cx.shared_state().await.assert_eq("something(fooˇ)");
3283    }
3284
3285    #[gpui::test]
3286    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3287        let mut cx = NeovimBackedTestContext::new(cx).await;
3288        cx.set_shared_state("ˇone\n  two\nthree").await;
3289        cx.simulate_shared_keystrokes("enter").await;
3290        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3291    }
3292
3293    #[gpui::test]
3294    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3295        let mut cx = NeovimBackedTestContext::new(cx).await;
3296        cx.set_shared_state("ˇ one\n two \nthree").await;
3297        cx.simulate_shared_keystrokes("g _").await;
3298        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3299
3300        cx.set_shared_state("ˇ one \n two \nthree").await;
3301        cx.simulate_shared_keystrokes("g _").await;
3302        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3303        cx.simulate_shared_keystrokes("2 g _").await;
3304        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3305    }
3306
3307    #[gpui::test]
3308    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3309        let mut cx = NeovimBackedTestContext::new(cx).await;
3310        let initial_state = indoc! {r"abc
3311          def
3312          paragraph
3313          the second
3314          third ˇand
3315          final"};
3316
3317        cx.set_shared_state(initial_state).await;
3318        cx.simulate_shared_keystrokes("shift-h").await;
3319        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3320          def
3321          paragraph
3322          the second
3323          third and
3324          final"});
3325
3326        // clip point
3327        cx.set_shared_state(indoc! {r"
3328          1 2 3
3329          4 5 6
3330          7 8 ˇ9
3331          "})
3332            .await;
3333        cx.simulate_shared_keystrokes("shift-h").await;
3334        cx.shared_state().await.assert_eq(indoc! {"
3335          1 2 ˇ3
3336          4 5 6
3337          7 8 9
3338          "});
3339
3340        cx.set_shared_state(indoc! {r"
3341          1 2 3
3342          4 5 6
3343          ˇ7 8 9
3344          "})
3345            .await;
3346        cx.simulate_shared_keystrokes("shift-h").await;
3347        cx.shared_state().await.assert_eq(indoc! {"
3348          ˇ1 2 3
3349          4 5 6
3350          7 8 9
3351          "});
3352
3353        cx.set_shared_state(indoc! {r"
3354          1 2 3
3355          4 5 ˇ6
3356          7 8 9"})
3357            .await;
3358        cx.simulate_shared_keystrokes("9 shift-h").await;
3359        cx.shared_state().await.assert_eq(indoc! {"
3360          1 2 3
3361          4 5 6
3362          7 8 ˇ9"});
3363    }
3364
3365    #[gpui::test]
3366    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3367        let mut cx = NeovimBackedTestContext::new(cx).await;
3368        let initial_state = indoc! {r"abˇc
3369          def
3370          paragraph
3371          the second
3372          third and
3373          final"};
3374
3375        cx.set_shared_state(initial_state).await;
3376        cx.simulate_shared_keystrokes("shift-m").await;
3377        cx.shared_state().await.assert_eq(indoc! {r"abc
3378          def
3379          paˇragraph
3380          the second
3381          third and
3382          final"});
3383
3384        cx.set_shared_state(indoc! {r"
3385          1 2 3
3386          4 5 6
3387          7 8 ˇ9
3388          "})
3389            .await;
3390        cx.simulate_shared_keystrokes("shift-m").await;
3391        cx.shared_state().await.assert_eq(indoc! {"
3392          1 2 3
3393          4 5 ˇ6
3394          7 8 9
3395          "});
3396        cx.set_shared_state(indoc! {r"
3397          1 2 3
3398          4 5 6
3399          ˇ7 8 9
3400          "})
3401            .await;
3402        cx.simulate_shared_keystrokes("shift-m").await;
3403        cx.shared_state().await.assert_eq(indoc! {"
3404          1 2 3
3405          ˇ4 5 6
3406          7 8 9
3407          "});
3408        cx.set_shared_state(indoc! {r"
3409          ˇ1 2 3
3410          4 5 6
3411          7 8 9
3412          "})
3413            .await;
3414        cx.simulate_shared_keystrokes("shift-m").await;
3415        cx.shared_state().await.assert_eq(indoc! {"
3416          1 2 3
3417          ˇ4 5 6
3418          7 8 9
3419          "});
3420        cx.set_shared_state(indoc! {r"
3421          1 2 3
3422          ˇ4 5 6
3423          7 8 9
3424          "})
3425            .await;
3426        cx.simulate_shared_keystrokes("shift-m").await;
3427        cx.shared_state().await.assert_eq(indoc! {"
3428          1 2 3
3429          ˇ4 5 6
3430          7 8 9
3431          "});
3432        cx.set_shared_state(indoc! {r"
3433          1 2 3
3434          4 5 ˇ6
3435          7 8 9
3436          "})
3437            .await;
3438        cx.simulate_shared_keystrokes("shift-m").await;
3439        cx.shared_state().await.assert_eq(indoc! {"
3440          1 2 3
3441          4 5 ˇ6
3442          7 8 9
3443          "});
3444    }
3445
3446    #[gpui::test]
3447    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3448        let mut cx = NeovimBackedTestContext::new(cx).await;
3449        let initial_state = indoc! {r"abc
3450          deˇf
3451          paragraph
3452          the second
3453          third and
3454          final"};
3455
3456        cx.set_shared_state(initial_state).await;
3457        cx.simulate_shared_keystrokes("shift-l").await;
3458        cx.shared_state().await.assert_eq(indoc! {r"abc
3459          def
3460          paragraph
3461          the second
3462          third and
3463          fiˇnal"});
3464
3465        cx.set_shared_state(indoc! {r"
3466          1 2 3
3467          4 5 ˇ6
3468          7 8 9
3469          "})
3470            .await;
3471        cx.simulate_shared_keystrokes("shift-l").await;
3472        cx.shared_state().await.assert_eq(indoc! {"
3473          1 2 3
3474          4 5 6
3475          7 8 9
3476          ˇ"});
3477
3478        cx.set_shared_state(indoc! {r"
3479          1 2 3
3480          ˇ4 5 6
3481          7 8 9
3482          "})
3483            .await;
3484        cx.simulate_shared_keystrokes("shift-l").await;
3485        cx.shared_state().await.assert_eq(indoc! {"
3486          1 2 3
3487          4 5 6
3488          7 8 9
3489          ˇ"});
3490
3491        cx.set_shared_state(indoc! {r"
3492          1 2 ˇ3
3493          4 5 6
3494          7 8 9
3495          "})
3496            .await;
3497        cx.simulate_shared_keystrokes("shift-l").await;
3498        cx.shared_state().await.assert_eq(indoc! {"
3499          1 2 3
3500          4 5 6
3501          7 8 9
3502          ˇ"});
3503
3504        cx.set_shared_state(indoc! {r"
3505          ˇ1 2 3
3506          4 5 6
3507          7 8 9
3508          "})
3509            .await;
3510        cx.simulate_shared_keystrokes("shift-l").await;
3511        cx.shared_state().await.assert_eq(indoc! {"
3512          1 2 3
3513          4 5 6
3514          7 8 9
3515          ˇ"});
3516
3517        cx.set_shared_state(indoc! {r"
3518          1 2 3
3519          4 5 ˇ6
3520          7 8 9
3521          "})
3522            .await;
3523        cx.simulate_shared_keystrokes("9 shift-l").await;
3524        cx.shared_state().await.assert_eq(indoc! {"
3525          1 2 ˇ3
3526          4 5 6
3527          7 8 9
3528          "});
3529    }
3530
3531    #[gpui::test]
3532    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3533        let mut cx = NeovimBackedTestContext::new(cx).await;
3534        cx.set_shared_state(indoc! {r"
3535        456 5ˇ67 678
3536        "})
3537            .await;
3538        cx.simulate_shared_keystrokes("g e").await;
3539        cx.shared_state().await.assert_eq(indoc! {"
3540        45ˇ6 567 678
3541        "});
3542
3543        // Test times
3544        cx.set_shared_state(indoc! {r"
3545        123 234 345
3546        456 5ˇ67 678
3547        "})
3548            .await;
3549        cx.simulate_shared_keystrokes("4 g e").await;
3550        cx.shared_state().await.assert_eq(indoc! {"
3551        12ˇ3 234 345
3552        456 567 678
3553        "});
3554
3555        // With punctuation
3556        cx.set_shared_state(indoc! {r"
3557        123 234 345
3558        4;5.6 5ˇ67 678
3559        789 890 901
3560        "})
3561            .await;
3562        cx.simulate_shared_keystrokes("g e").await;
3563        cx.shared_state().await.assert_eq(indoc! {"
3564          123 234 345
3565          4;5.ˇ6 567 678
3566          789 890 901
3567        "});
3568
3569        // With punctuation and count
3570        cx.set_shared_state(indoc! {r"
3571        123 234 345
3572        4;5.6 5ˇ67 678
3573        789 890 901
3574        "})
3575            .await;
3576        cx.simulate_shared_keystrokes("5 g e").await;
3577        cx.shared_state().await.assert_eq(indoc! {"
3578          123 234 345
3579          ˇ4;5.6 567 678
3580          789 890 901
3581        "});
3582
3583        // newlines
3584        cx.set_shared_state(indoc! {r"
3585        123 234 345
3586
3587        78ˇ9 890 901
3588        "})
3589            .await;
3590        cx.simulate_shared_keystrokes("g e").await;
3591        cx.shared_state().await.assert_eq(indoc! {"
3592          123 234 345
3593          ˇ
3594          789 890 901
3595        "});
3596        cx.simulate_shared_keystrokes("g e").await;
3597        cx.shared_state().await.assert_eq(indoc! {"
3598          123 234 34ˇ5
3599
3600          789 890 901
3601        "});
3602
3603        // With punctuation
3604        cx.set_shared_state(indoc! {r"
3605        123 234 345
3606        4;5.ˇ6 567 678
3607        789 890 901
3608        "})
3609            .await;
3610        cx.simulate_shared_keystrokes("g shift-e").await;
3611        cx.shared_state().await.assert_eq(indoc! {"
3612          123 234 34ˇ5
3613          4;5.6 567 678
3614          789 890 901
3615        "});
3616    }
3617
3618    #[gpui::test]
3619    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3620        let mut cx = NeovimBackedTestContext::new(cx).await;
3621
3622        cx.set_shared_state(indoc! {"
3623            fn aˇ() {
3624              return
3625            }
3626        "})
3627            .await;
3628        cx.simulate_shared_keystrokes("v $ %").await;
3629        cx.shared_state().await.assert_eq(indoc! {"
3630            fn a«() {
3631              return
3632            }ˇ»
3633        "});
3634    }
3635
3636    #[gpui::test]
3637    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3638        let mut cx = VimTestContext::new(cx, true).await;
3639
3640        cx.set_state(
3641            indoc! {"
3642                struct Foo {
3643                ˇ
3644                }
3645            "},
3646            Mode::Normal,
3647        );
3648
3649        cx.update_editor(|editor, _window, cx| {
3650            let range = editor.selections.newest_anchor().range();
3651            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3652            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3653            editor.splice_inlays(&[], vec![inlay], cx);
3654        });
3655
3656        cx.simulate_keystrokes("j");
3657        cx.assert_state(
3658            indoc! {"
3659                struct Foo {
3660
3661                ˇ}
3662            "},
3663            Mode::Normal,
3664        );
3665    }
3666
3667    #[gpui::test]
3668    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3669        let mut cx = VimTestContext::new(cx, true).await;
3670
3671        cx.set_state(
3672            indoc! {"
3673            ˇstruct Foo {
3674
3675            }
3676        "},
3677            Mode::Normal,
3678        );
3679        cx.update_editor(|editor, _window, cx| {
3680            let snapshot = editor.buffer().read(cx).snapshot(cx);
3681            let end_of_line =
3682                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3683            let inlay_text = " hint";
3684            let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3685            editor.splice_inlays(&[], vec![inlay], cx);
3686        });
3687        cx.simulate_keystrokes("$");
3688        cx.assert_state(
3689            indoc! {"
3690            struct Foo ˇ{
3691
3692            }
3693        "},
3694            Mode::Normal,
3695        );
3696    }
3697
3698    #[gpui::test]
3699    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3700        let mut cx = NeovimBackedTestContext::new(cx).await;
3701        // Normal mode
3702        cx.set_shared_state(indoc! {"
3703            The ˇquick brown
3704            fox jumps over
3705            the lazy dog
3706            The quick brown
3707            fox jumps over
3708            the lazy dog
3709            The quick brown
3710            fox jumps over
3711            the lazy dog"})
3712            .await;
3713        cx.simulate_shared_keystrokes("2 0 %").await;
3714        cx.shared_state().await.assert_eq(indoc! {"
3715            The quick brown
3716            fox ˇjumps over
3717            the lazy dog
3718            The quick brown
3719            fox jumps over
3720            the lazy dog
3721            The quick brown
3722            fox jumps over
3723            the lazy dog"});
3724
3725        cx.simulate_shared_keystrokes("2 5 %").await;
3726        cx.shared_state().await.assert_eq(indoc! {"
3727            The quick brown
3728            fox jumps over
3729            the ˇlazy dog
3730            The quick brown
3731            fox jumps over
3732            the lazy dog
3733            The quick brown
3734            fox jumps over
3735            the lazy dog"});
3736
3737        cx.simulate_shared_keystrokes("7 5 %").await;
3738        cx.shared_state().await.assert_eq(indoc! {"
3739            The quick brown
3740            fox jumps over
3741            the lazy dog
3742            The quick brown
3743            fox jumps over
3744            the lazy dog
3745            The ˇquick brown
3746            fox jumps over
3747            the lazy dog"});
3748
3749        // Visual 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("v 5 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 jˇ»umps over
3768            the lazy dog
3769            The quick brown
3770            fox jumps over
3771            the lazy dog"});
3772
3773        cx.set_shared_state(indoc! {"
3774            The ˇquick brown
3775            fox jumps over
3776            the lazy dog
3777            The quick brown
3778            fox jumps over
3779            the lazy dog
3780            The quick brown
3781            fox jumps over
3782            the lazy dog"})
3783            .await;
3784        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3785        cx.shared_state().await.assert_eq(indoc! {"
3786            The «quick brown
3787            fox jumps over
3788            the lazy dog
3789            The quick brown
3790            fox jumps over
3791            the lazy dog
3792            The quick brown
3793            fox jumps over
3794            the lˇ»azy dog"});
3795    }
3796
3797    #[gpui::test]
3798    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3799        let mut cx = NeovimBackedTestContext::new(cx).await;
3800
3801        cx.set_shared_state("ˇπππππ").await;
3802        cx.simulate_shared_keystrokes("3 space").await;
3803        cx.shared_state().await.assert_eq("πππˇππ");
3804    }
3805
3806    #[gpui::test]
3807    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3808        let mut cx = NeovimBackedTestContext::new(cx).await;
3809
3810        cx.set_shared_state(indoc! {"
3811            ππππˇπ
3812            πanotherline"})
3813            .await;
3814        cx.simulate_shared_keystrokes("4 space").await;
3815        cx.shared_state().await.assert_eq(indoc! {"
3816            πππππ
3817            πanˇotherline"});
3818    }
3819
3820    #[gpui::test]
3821    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3822        let mut cx = NeovimBackedTestContext::new(cx).await;
3823
3824        cx.set_shared_state(indoc! {"
3825                        ππππ
3826                        πanˇotherline"})
3827            .await;
3828        cx.simulate_shared_keystrokes("4 backspace").await;
3829        cx.shared_state().await.assert_eq(indoc! {"
3830                        πππˇπ
3831                        πanotherline"});
3832    }
3833
3834    #[gpui::test]
3835    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3836        let mut cx = VimTestContext::new(cx, true).await;
3837        cx.set_state(
3838            indoc! {
3839                "func empty(a string) bool {
3840                     ˇif a == \"\" {
3841                         return true
3842                     }
3843                     return false
3844                }"
3845            },
3846            Mode::Normal,
3847        );
3848        cx.simulate_keystrokes("[ -");
3849        cx.assert_state(
3850            indoc! {
3851                "ˇfunc empty(a string) bool {
3852                     if a == \"\" {
3853                         return true
3854                     }
3855                     return false
3856                }"
3857            },
3858            Mode::Normal,
3859        );
3860        cx.simulate_keystrokes("] =");
3861        cx.assert_state(
3862            indoc! {
3863                "func empty(a string) bool {
3864                     if a == \"\" {
3865                         return true
3866                     }
3867                     return false
3868                ˇ}"
3869            },
3870            Mode::Normal,
3871        );
3872        cx.simulate_keystrokes("[ +");
3873        cx.assert_state(
3874            indoc! {
3875                "func empty(a string) bool {
3876                     if a == \"\" {
3877                         return true
3878                     }
3879                     ˇreturn false
3880                }"
3881            },
3882            Mode::Normal,
3883        );
3884        cx.simulate_keystrokes("2 [ =");
3885        cx.assert_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    }
3921
3922    #[gpui::test]
3923    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
3924        let mut cx = NeovimBackedTestContext::new(cx).await;
3925        cx.set_shared_state("abˇc").await;
3926        cx.simulate_shared_keystrokes("delete").await;
3927        cx.shared_state().await.assert_eq("aˇb");
3928    }
3929
3930    #[gpui::test]
3931    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
3932        let mut cx = NeovimBackedTestContext::new(cx).await;
3933
3934        cx.set_shared_state(indoc! {"
3935             ˇthe quick brown fox
3936             jumped over the lazy dog"})
3937            .await;
3938        cx.simulate_shared_keystrokes("d v 0").await;
3939        cx.shared_state().await.assert_eq(indoc! {"
3940             ˇhe quick brown fox
3941             jumped over the lazy dog"});
3942        assert_eq!(cx.cx.forced_motion(), false);
3943
3944        cx.set_shared_state(indoc! {"
3945            the quick bˇrown fox
3946            jumped over the lazy dog"})
3947            .await;
3948        cx.simulate_shared_keystrokes("d v 0").await;
3949        cx.shared_state().await.assert_eq(indoc! {"
3950            ˇown fox
3951            jumped over the lazy dog"});
3952        assert_eq!(cx.cx.forced_motion(), false);
3953
3954        cx.set_shared_state(indoc! {"
3955            the quick brown foˇx
3956            jumped over the lazy dog"})
3957            .await;
3958        cx.simulate_shared_keystrokes("d v 0").await;
3959        cx.shared_state().await.assert_eq(indoc! {"
3960            ˇ
3961            jumped over the lazy dog"});
3962        assert_eq!(cx.cx.forced_motion(), false);
3963    }
3964
3965    #[gpui::test]
3966    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
3967        let mut cx = NeovimBackedTestContext::new(cx).await;
3968
3969        cx.set_shared_state(indoc! {"
3970             ˇthe quick brown fox
3971             jumped over the lazy dog"})
3972            .await;
3973        cx.simulate_shared_keystrokes("d v g shift-m").await;
3974        cx.shared_state().await.assert_eq(indoc! {"
3975             ˇbrown fox
3976             jumped over the lazy dog"});
3977        assert_eq!(cx.cx.forced_motion(), false);
3978
3979        cx.set_shared_state(indoc! {"
3980            the quick bˇrown fox
3981            jumped over the lazy dog"})
3982            .await;
3983        cx.simulate_shared_keystrokes("d v g shift-m").await;
3984        cx.shared_state().await.assert_eq(indoc! {"
3985            the quickˇown fox
3986            jumped over the lazy dog"});
3987        assert_eq!(cx.cx.forced_motion(), false);
3988
3989        cx.set_shared_state(indoc! {"
3990            the quick brown foˇx
3991            jumped over the lazy dog"})
3992            .await;
3993        cx.simulate_shared_keystrokes("d v g shift-m").await;
3994        cx.shared_state().await.assert_eq(indoc! {"
3995            the quicˇk
3996            jumped over the lazy dog"});
3997        assert_eq!(cx.cx.forced_motion(), false);
3998
3999        cx.set_shared_state(indoc! {"
4000            ˇthe quick brown fox
4001            jumped over the lazy dog"})
4002            .await;
4003        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4004        cx.shared_state().await.assert_eq(indoc! {"
4005            ˇ fox
4006            jumped over the lazy dog"});
4007        assert_eq!(cx.cx.forced_motion(), false);
4008
4009        cx.set_shared_state(indoc! {"
4010            ˇthe quick brown fox
4011            jumped over the lazy dog"})
4012            .await;
4013        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4014        cx.shared_state().await.assert_eq(indoc! {"
4015            ˇuick brown fox
4016            jumped over the lazy dog"});
4017        assert_eq!(cx.cx.forced_motion(), false);
4018    }
4019
4020    #[gpui::test]
4021    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4022        let mut cx = NeovimBackedTestContext::new(cx).await;
4023
4024        cx.set_shared_state(indoc! {"
4025             the quick brown foˇx
4026             jumped over the lazy dog"})
4027            .await;
4028        cx.simulate_shared_keystrokes("d v $").await;
4029        cx.shared_state().await.assert_eq(indoc! {"
4030             the quick brown foˇx
4031             jumped over the lazy dog"});
4032        assert_eq!(cx.cx.forced_motion(), false);
4033
4034        cx.set_shared_state(indoc! {"
4035             ˇthe quick brown fox
4036             jumped over the lazy dog"})
4037            .await;
4038        cx.simulate_shared_keystrokes("d v $").await;
4039        cx.shared_state().await.assert_eq(indoc! {"
4040             ˇx
4041             jumped over the lazy dog"});
4042        assert_eq!(cx.cx.forced_motion(), false);
4043    }
4044
4045    #[gpui::test]
4046    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4047        let mut cx = NeovimBackedTestContext::new(cx).await;
4048
4049        cx.set_shared_state(indoc! {"
4050               ˇthe quick brown fox
4051               jumped over the lazy dog"})
4052            .await;
4053        cx.simulate_shared_keystrokes("y v j p").await;
4054        cx.shared_state().await.assert_eq(indoc! {"
4055               the quick brown fox
4056               ˇthe quick brown fox
4057               jumped over the lazy dog"});
4058        assert_eq!(cx.cx.forced_motion(), false);
4059
4060        cx.set_shared_state(indoc! {"
4061              the quick bˇrown fox
4062              jumped over the lazy dog"})
4063            .await;
4064        cx.simulate_shared_keystrokes("y v j p").await;
4065        cx.shared_state().await.assert_eq(indoc! {"
4066              the quick brˇrown fox
4067              jumped overown fox
4068              jumped over the lazy dog"});
4069        assert_eq!(cx.cx.forced_motion(), false);
4070
4071        cx.set_shared_state(indoc! {"
4072             the quick brown foˇx
4073             jumped over the lazy dog"})
4074            .await;
4075        cx.simulate_shared_keystrokes("y v j p").await;
4076        cx.shared_state().await.assert_eq(indoc! {"
4077             the quick brown foxˇx
4078             jumped over the la
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             jˇumped over the lazy dog"})
4085            .await;
4086        cx.simulate_shared_keystrokes("y v k p").await;
4087        cx.shared_state().await.assert_eq(indoc! {"
4088            thˇhe quick brown fox
4089            je quick brown fox
4090            jumped over the lazy dog"});
4091        assert_eq!(cx.cx.forced_motion(), false);
4092    }
4093
4094    #[gpui::test]
4095    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4096        let mut cx = NeovimBackedTestContext::new(cx).await;
4097
4098        cx.set_shared_state(indoc! {"
4099              ˇthe quick brown fox
4100              jumped over the lazy dog"})
4101            .await;
4102        cx.simulate_shared_keystrokes("d v e").await;
4103        cx.shared_state().await.assert_eq(indoc! {"
4104              ˇe 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("d v e").await;
4113        cx.shared_state().await.assert_eq(indoc! {"
4114              the quick bˇn fox
4115              jumped over the lazy dog"});
4116        assert_eq!(cx.cx.forced_motion(), false);
4117
4118        cx.set_shared_state(indoc! {"
4119             the quick brown foˇx
4120             jumped over the lazy dog"})
4121            .await;
4122        cx.simulate_shared_keystrokes("d v e").await;
4123        cx.shared_state().await.assert_eq(indoc! {"
4124        the quick brown foˇd over the lazy dog"});
4125        assert_eq!(cx.cx.forced_motion(), false);
4126    }
4127}