motion.rs

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