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