motion.rs

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