motion.rs

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