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