motion.rs

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