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