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