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