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
1892pub(crate) fn 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
1938pub(crate) fn sentence_forwards(
1939    map: &DisplaySnapshot,
1940    point: DisplayPoint,
1941    mut times: usize,
1942) -> DisplayPoint {
1943    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1944    let mut chars = map.buffer_chars_at(start).peekable();
1945
1946    let mut was_newline = map
1947        .reverse_buffer_chars_at(start)
1948        .next()
1949        .is_some_and(|(c, _)| c == '\n')
1950        && chars.peek().is_some_and(|(c, _)| *c == '\n');
1951
1952    while let Some((ch, offset)) = chars.next() {
1953        if was_newline && ch == '\n' {
1954            continue;
1955        }
1956        let start_of_next_sentence = if was_newline {
1957            Some(next_non_blank(map, offset))
1958        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1959            Some(next_non_blank(map, offset + ch.len_utf8()))
1960        } else if ch == '.' || ch == '?' || ch == '!' {
1961            start_of_next_sentence(map, offset + ch.len_utf8())
1962        } else {
1963            None
1964        };
1965
1966        if let Some(start_of_next_sentence) = start_of_next_sentence {
1967            times = times.saturating_sub(1);
1968            if times == 0 {
1969                return map.clip_point(
1970                    start_of_next_sentence
1971                        .to_offset(&map.buffer_snapshot)
1972                        .to_display_point(map),
1973                    Bias::Right,
1974                );
1975            }
1976        }
1977
1978        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1979    }
1980
1981    map.max_point()
1982}
1983
1984fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1985    for (c, o) in map.buffer_chars_at(start) {
1986        if c == '\n' || !c.is_whitespace() {
1987            return o;
1988        }
1989    }
1990
1991    map.buffer_snapshot.len()
1992}
1993
1994// given the offset after a ., !, or ? find the start of the next sentence.
1995// if this is not a sentence boundary, returns None.
1996fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1997    let chars = map.buffer_chars_at(end_of_sentence);
1998    let mut seen_space = false;
1999
2000    for (char, offset) in chars {
2001        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2002            continue;
2003        }
2004
2005        if char == '\n' && seen_space {
2006            return Some(offset);
2007        } else if char.is_whitespace() {
2008            seen_space = true;
2009        } else if seen_space {
2010            return Some(offset);
2011        } else {
2012            return None;
2013        }
2014    }
2015
2016    Some(map.buffer_snapshot.len())
2017}
2018
2019fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2020    let point = map.display_point_to_point(display_point, Bias::Left);
2021    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2022        return display_point;
2023    };
2024    let offset = excerpt.buffer().point_to_offset(
2025        excerpt
2026            .buffer()
2027            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2028    );
2029    let buffer_range = excerpt.buffer_range();
2030    if offset >= buffer_range.start && offset <= buffer_range.end {
2031        let point = map
2032            .buffer_snapshot
2033            .offset_to_point(excerpt.map_offset_from_buffer(offset));
2034        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2035    }
2036    let mut last_position = None;
2037    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2038        let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2039            ..language::ToOffset::to_offset(&range.context.end, &buffer);
2040        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2041            let text_anchor = buffer.anchor_after(offset);
2042            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2043            return anchor.to_display_point(map);
2044        } else if offset <= excerpt_range.start {
2045            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2046            return anchor.to_display_point(map);
2047        } else {
2048            last_position = Some(Anchor::in_buffer(
2049                excerpt,
2050                buffer.remote_id(),
2051                range.context.end,
2052            ));
2053        }
2054    }
2055
2056    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2057    last_point.column = point.column;
2058
2059    map.clip_point(
2060        map.point_to_display_point(
2061            map.buffer_snapshot.clip_point(point, Bias::Left),
2062            Bias::Left,
2063        ),
2064        Bias::Left,
2065    )
2066}
2067
2068fn start_of_document(
2069    map: &DisplaySnapshot,
2070    display_point: DisplayPoint,
2071    maybe_times: Option<usize>,
2072) -> DisplayPoint {
2073    if let Some(times) = maybe_times {
2074        return go_to_line(map, display_point, times);
2075    }
2076
2077    let point = map.display_point_to_point(display_point, Bias::Left);
2078    let mut first_point = Point::zero();
2079    first_point.column = point.column;
2080
2081    map.clip_point(
2082        map.point_to_display_point(
2083            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2084            Bias::Left,
2085        ),
2086        Bias::Left,
2087    )
2088}
2089
2090fn end_of_document(
2091    map: &DisplaySnapshot,
2092    display_point: DisplayPoint,
2093    maybe_times: Option<usize>,
2094) -> DisplayPoint {
2095    if let Some(times) = maybe_times {
2096        return go_to_line(map, display_point, times);
2097    };
2098    let point = map.display_point_to_point(display_point, Bias::Left);
2099    let mut last_point = map.buffer_snapshot.max_point();
2100    last_point.column = point.column;
2101
2102    map.clip_point(
2103        map.point_to_display_point(
2104            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2105            Bias::Left,
2106        ),
2107        Bias::Left,
2108    )
2109}
2110
2111fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2112    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2113    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2114
2115    if head > outer.start && head < inner.start {
2116        let mut offset = inner.end.to_offset(map, Bias::Left);
2117        for c in map.buffer_snapshot.chars_at(offset) {
2118            if c == '/' || c == '\n' || c == '>' {
2119                return Some(offset.to_display_point(map));
2120            }
2121            offset += c.len_utf8();
2122        }
2123    } else {
2124        let mut offset = outer.start.to_offset(map, Bias::Left);
2125        for c in map.buffer_snapshot.chars_at(offset) {
2126            offset += c.len_utf8();
2127            if c == '<' || c == '\n' {
2128                return Some(offset.to_display_point(map));
2129            }
2130        }
2131    }
2132
2133    return None;
2134}
2135
2136fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2137    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2138    let display_point = map.clip_at_line_end(display_point);
2139    let point = display_point.to_point(map);
2140    let offset = point.to_offset(&map.buffer_snapshot);
2141
2142    // Ensure the range is contained by the current line.
2143    let mut line_end = map.next_line_boundary(point).0;
2144    if line_end == point {
2145        line_end = map.max_point().to_point(map);
2146    }
2147
2148    let line_range = map.prev_line_boundary(point).0..line_end;
2149    let visible_line_range =
2150        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2151    let ranges = map
2152        .buffer_snapshot
2153        .bracket_ranges(visible_line_range.clone());
2154    if let Some(ranges) = ranges {
2155        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2156            ..line_range.end.to_offset(&map.buffer_snapshot);
2157        let mut closest_pair_destination = None;
2158        let mut closest_distance = usize::MAX;
2159
2160        for (open_range, close_range) in ranges {
2161            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2162                if offset > open_range.start && offset < close_range.start {
2163                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2164                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2165                        return display_point;
2166                    }
2167                    if let Some(tag) = matching_tag(map, display_point) {
2168                        return tag;
2169                    }
2170                } else if close_range.contains(&offset) {
2171                    return open_range.start.to_display_point(map);
2172                } else if open_range.contains(&offset) {
2173                    return (close_range.end - 1).to_display_point(map);
2174                }
2175            }
2176
2177            if (open_range.contains(&offset) || open_range.start >= offset)
2178                && line_range.contains(&open_range.start)
2179            {
2180                let distance = open_range.start.saturating_sub(offset);
2181                if distance < closest_distance {
2182                    closest_pair_destination = Some(close_range.start);
2183                    closest_distance = distance;
2184                    continue;
2185                }
2186            }
2187
2188            if (close_range.contains(&offset) || close_range.start >= offset)
2189                && line_range.contains(&close_range.start)
2190            {
2191                let distance = close_range.start.saturating_sub(offset);
2192                if distance < closest_distance {
2193                    closest_pair_destination = Some(open_range.start);
2194                    closest_distance = distance;
2195                    continue;
2196                }
2197            }
2198
2199            continue;
2200        }
2201
2202        closest_pair_destination
2203            .map(|destination| destination.to_display_point(map))
2204            .unwrap_or(display_point)
2205    } else {
2206        display_point
2207    }
2208}
2209
2210// Go to {count} percentage in the file, on the first
2211// non-blank in the line linewise.  To compute the new
2212// line number this formula is used:
2213// ({count} * number-of-lines + 99) / 100
2214//
2215// https://neovim.io/doc/user/motion.html#N%25
2216fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2217    let total_lines = map.buffer_snapshot.max_point().row + 1;
2218    let target_line = (count * total_lines as usize + 99) / 100;
2219    let target_point = DisplayPoint::new(
2220        DisplayRow(target_line.saturating_sub(1) as u32),
2221        point.column(),
2222    );
2223    map.clip_point(target_point, Bias::Left)
2224}
2225
2226fn unmatched_forward(
2227    map: &DisplaySnapshot,
2228    mut display_point: DisplayPoint,
2229    char: char,
2230    times: usize,
2231) -> DisplayPoint {
2232    for _ in 0..times {
2233        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2234        let point = display_point.to_point(map);
2235        let offset = point.to_offset(&map.buffer_snapshot);
2236
2237        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2238        let Some(ranges) = ranges else { break };
2239        let mut closest_closing_destination = None;
2240        let mut closest_distance = usize::MAX;
2241
2242        for (_, close_range) in ranges {
2243            if close_range.start > offset {
2244                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2245                if Some(char) == chars.next() {
2246                    let distance = close_range.start - offset;
2247                    if distance < closest_distance {
2248                        closest_closing_destination = Some(close_range.start);
2249                        closest_distance = distance;
2250                        continue;
2251                    }
2252                }
2253            }
2254        }
2255
2256        let new_point = closest_closing_destination
2257            .map(|destination| destination.to_display_point(map))
2258            .unwrap_or(display_point);
2259        if new_point == display_point {
2260            break;
2261        }
2262        display_point = new_point;
2263    }
2264    return display_point;
2265}
2266
2267fn unmatched_backward(
2268    map: &DisplaySnapshot,
2269    mut display_point: DisplayPoint,
2270    char: char,
2271    times: usize,
2272) -> DisplayPoint {
2273    for _ in 0..times {
2274        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2275        let point = display_point.to_point(map);
2276        let offset = point.to_offset(&map.buffer_snapshot);
2277
2278        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2279        let Some(ranges) = ranges else {
2280            break;
2281        };
2282
2283        let mut closest_starting_destination = None;
2284        let mut closest_distance = usize::MAX;
2285
2286        for (start_range, _) in ranges {
2287            if start_range.start < offset {
2288                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2289                if Some(char) == chars.next() {
2290                    let distance = offset - start_range.start;
2291                    if distance < closest_distance {
2292                        closest_starting_destination = Some(start_range.start);
2293                        closest_distance = distance;
2294                        continue;
2295                    }
2296                }
2297            }
2298        }
2299
2300        let new_point = closest_starting_destination
2301            .map(|destination| destination.to_display_point(map))
2302            .unwrap_or(display_point);
2303        if new_point == display_point {
2304            break;
2305        } else {
2306            display_point = new_point;
2307        }
2308    }
2309    display_point
2310}
2311
2312fn find_forward(
2313    map: &DisplaySnapshot,
2314    from: DisplayPoint,
2315    before: bool,
2316    target: char,
2317    times: usize,
2318    mode: FindRange,
2319    smartcase: bool,
2320) -> Option<DisplayPoint> {
2321    let mut to = from;
2322    let mut found = false;
2323
2324    for _ in 0..times {
2325        found = false;
2326        let new_to = find_boundary(map, to, mode, |_, right| {
2327            found = is_character_match(target, right, smartcase);
2328            found
2329        });
2330        if to == new_to {
2331            break;
2332        }
2333        to = new_to;
2334    }
2335
2336    if found {
2337        if before && to.column() > 0 {
2338            *to.column_mut() -= 1;
2339            Some(map.clip_point(to, Bias::Left))
2340        } else {
2341            Some(to)
2342        }
2343    } else {
2344        None
2345    }
2346}
2347
2348fn find_backward(
2349    map: &DisplaySnapshot,
2350    from: DisplayPoint,
2351    after: bool,
2352    target: char,
2353    times: usize,
2354    mode: FindRange,
2355    smartcase: bool,
2356) -> DisplayPoint {
2357    let mut to = from;
2358
2359    for _ in 0..times {
2360        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2361            is_character_match(target, right, smartcase)
2362        });
2363        if to == new_to {
2364            break;
2365        }
2366        to = new_to;
2367    }
2368
2369    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2370    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2371        if after {
2372            *to.column_mut() += 1;
2373            map.clip_point(to, Bias::Right)
2374        } else {
2375            to
2376        }
2377    } else {
2378        from
2379    }
2380}
2381
2382fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2383    if smartcase {
2384        if target.is_uppercase() {
2385            target == other
2386        } else {
2387            target == other.to_ascii_lowercase()
2388        }
2389    } else {
2390        target == other
2391    }
2392}
2393
2394fn sneak(
2395    map: &DisplaySnapshot,
2396    from: DisplayPoint,
2397    first_target: char,
2398    second_target: char,
2399    times: usize,
2400    smartcase: bool,
2401) -> Option<DisplayPoint> {
2402    let mut to = from;
2403    let mut found = false;
2404
2405    for _ in 0..times {
2406        found = false;
2407        let new_to = find_boundary(
2408            map,
2409            movement::right(map, to),
2410            FindRange::MultiLine,
2411            |left, right| {
2412                found = is_character_match(first_target, left, smartcase)
2413                    && is_character_match(second_target, right, smartcase);
2414                found
2415            },
2416        );
2417        if to == new_to {
2418            break;
2419        }
2420        to = new_to;
2421    }
2422
2423    if found {
2424        Some(movement::left(map, to))
2425    } else {
2426        None
2427    }
2428}
2429
2430fn sneak_backward(
2431    map: &DisplaySnapshot,
2432    from: DisplayPoint,
2433    first_target: char,
2434    second_target: char,
2435    times: usize,
2436    smartcase: bool,
2437) -> Option<DisplayPoint> {
2438    let mut to = from;
2439    let mut found = false;
2440
2441    for _ in 0..times {
2442        found = false;
2443        let new_to =
2444            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2445                found = is_character_match(first_target, left, smartcase)
2446                    && is_character_match(second_target, right, smartcase);
2447                found
2448            });
2449        if to == new_to {
2450            break;
2451        }
2452        to = new_to;
2453    }
2454
2455    if found {
2456        Some(movement::left(map, to))
2457    } else {
2458        None
2459    }
2460}
2461
2462fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2463    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2464    first_non_whitespace(map, false, correct_line)
2465}
2466
2467fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2468    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2469    first_non_whitespace(map, false, correct_line)
2470}
2471
2472fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2473    let correct_line = start_of_relative_buffer_row(map, point, 0);
2474    right(map, correct_line, times.saturating_sub(1))
2475}
2476
2477pub(crate) fn next_line_end(
2478    map: &DisplaySnapshot,
2479    mut point: DisplayPoint,
2480    times: usize,
2481) -> DisplayPoint {
2482    if times > 1 {
2483        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2484    }
2485    end_of_line(map, false, point, 1)
2486}
2487
2488fn window_top(
2489    map: &DisplaySnapshot,
2490    point: DisplayPoint,
2491    text_layout_details: &TextLayoutDetails,
2492    mut times: usize,
2493) -> (DisplayPoint, SelectionGoal) {
2494    let first_visible_line = text_layout_details
2495        .scroll_anchor
2496        .anchor
2497        .to_display_point(map);
2498
2499    if first_visible_line.row() != DisplayRow(0)
2500        && text_layout_details.vertical_scroll_margin as usize > times
2501    {
2502        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2503    }
2504
2505    if let Some(visible_rows) = text_layout_details.visible_rows {
2506        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2507        let new_row = (first_visible_line.row().0 + (times as u32))
2508            .min(bottom_row)
2509            .min(map.max_point().row().0);
2510        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2511
2512        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2513        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2514    } else {
2515        let new_row =
2516            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2517        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2518
2519        let new_point = DisplayPoint::new(new_row, new_col);
2520        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2521    }
2522}
2523
2524fn window_middle(
2525    map: &DisplaySnapshot,
2526    point: DisplayPoint,
2527    text_layout_details: &TextLayoutDetails,
2528) -> (DisplayPoint, SelectionGoal) {
2529    if let Some(visible_rows) = text_layout_details.visible_rows {
2530        let first_visible_line = text_layout_details
2531            .scroll_anchor
2532            .anchor
2533            .to_display_point(map);
2534
2535        let max_visible_rows =
2536            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2537
2538        let new_row =
2539            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2540        let new_row = DisplayRow(new_row);
2541        let new_col = point.column().min(map.line_len(new_row));
2542        let new_point = DisplayPoint::new(new_row, new_col);
2543        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2544    } else {
2545        (point, SelectionGoal::None)
2546    }
2547}
2548
2549fn window_bottom(
2550    map: &DisplaySnapshot,
2551    point: DisplayPoint,
2552    text_layout_details: &TextLayoutDetails,
2553    mut times: usize,
2554) -> (DisplayPoint, SelectionGoal) {
2555    if let Some(visible_rows) = text_layout_details.visible_rows {
2556        let first_visible_line = text_layout_details
2557            .scroll_anchor
2558            .anchor
2559            .to_display_point(map);
2560        let bottom_row = first_visible_line.row().0
2561            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2562        if bottom_row < map.max_point().row().0
2563            && text_layout_details.vertical_scroll_margin as usize > times
2564        {
2565            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2566        }
2567        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2568        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2569        {
2570            first_visible_line.row()
2571        } else {
2572            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2573        };
2574        let new_col = point.column().min(map.line_len(new_row));
2575        let new_point = DisplayPoint::new(new_row, new_col);
2576        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2577    } else {
2578        (point, SelectionGoal::None)
2579    }
2580}
2581
2582fn method_motion(
2583    map: &DisplaySnapshot,
2584    mut display_point: DisplayPoint,
2585    times: usize,
2586    direction: Direction,
2587    is_start: bool,
2588) -> DisplayPoint {
2589    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2590        return display_point;
2591    };
2592
2593    for _ in 0..times {
2594        let point = map.display_point_to_point(display_point, Bias::Left);
2595        let offset = point.to_offset(&map.buffer_snapshot);
2596        let range = if direction == Direction::Prev {
2597            0..offset
2598        } else {
2599            offset..buffer.len()
2600        };
2601
2602        let possibilities = buffer
2603            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2604            .filter_map(|(range, object)| {
2605                if !matches!(object, language::TextObject::AroundFunction) {
2606                    return None;
2607                }
2608
2609                let relevant = if is_start { range.start } else { range.end };
2610                if direction == Direction::Prev && relevant < offset {
2611                    Some(relevant)
2612                } else if direction == Direction::Next && relevant > offset + 1 {
2613                    Some(relevant)
2614                } else {
2615                    None
2616                }
2617            });
2618
2619        let dest = if direction == Direction::Prev {
2620            possibilities.max().unwrap_or(offset)
2621        } else {
2622            possibilities.min().unwrap_or(offset)
2623        };
2624        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2625        if new_point == display_point {
2626            break;
2627        }
2628        display_point = new_point;
2629    }
2630    display_point
2631}
2632
2633fn comment_motion(
2634    map: &DisplaySnapshot,
2635    mut display_point: DisplayPoint,
2636    times: usize,
2637    direction: Direction,
2638) -> DisplayPoint {
2639    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2640        return display_point;
2641    };
2642
2643    for _ in 0..times {
2644        let point = map.display_point_to_point(display_point, Bias::Left);
2645        let offset = point.to_offset(&map.buffer_snapshot);
2646        let range = if direction == Direction::Prev {
2647            0..offset
2648        } else {
2649            offset..buffer.len()
2650        };
2651
2652        let possibilities = buffer
2653            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2654            .filter_map(|(range, object)| {
2655                if !matches!(object, language::TextObject::AroundComment) {
2656                    return None;
2657                }
2658
2659                let relevant = if direction == Direction::Prev {
2660                    range.start
2661                } else {
2662                    range.end
2663                };
2664                if direction == Direction::Prev && relevant < offset {
2665                    Some(relevant)
2666                } else if direction == Direction::Next && relevant > offset + 1 {
2667                    Some(relevant)
2668                } else {
2669                    None
2670                }
2671            });
2672
2673        let dest = if direction == Direction::Prev {
2674            possibilities.max().unwrap_or(offset)
2675        } else {
2676            possibilities.min().unwrap_or(offset)
2677        };
2678        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2679        if new_point == display_point {
2680            break;
2681        }
2682        display_point = new_point;
2683    }
2684
2685    display_point
2686}
2687
2688fn section_motion(
2689    map: &DisplaySnapshot,
2690    mut display_point: DisplayPoint,
2691    times: usize,
2692    direction: Direction,
2693    is_start: bool,
2694) -> DisplayPoint {
2695    if map.buffer_snapshot.as_singleton().is_some() {
2696        for _ in 0..times {
2697            let offset = map
2698                .display_point_to_point(display_point, Bias::Left)
2699                .to_offset(&map.buffer_snapshot);
2700            let range = if direction == Direction::Prev {
2701                0..offset
2702            } else {
2703                offset..map.buffer_snapshot.len()
2704            };
2705
2706            // we set a max start depth here because we want a section to only be "top level"
2707            // similar to vim's default of '{' in the first column.
2708            // (and without it, ]] at the start of editor.rs is -very- slow)
2709            let mut possibilities = map
2710                .buffer_snapshot
2711                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2712                .filter(|(_, object)| {
2713                    matches!(
2714                        object,
2715                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2716                    )
2717                })
2718                .collect::<Vec<_>>();
2719            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2720            let mut prev_end = None;
2721            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2722                if t == language::TextObject::AroundFunction
2723                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2724                {
2725                    return None;
2726                }
2727                prev_end = Some(range.end);
2728
2729                let relevant = if is_start { range.start } else { range.end };
2730                if direction == Direction::Prev && relevant < offset {
2731                    Some(relevant)
2732                } else if direction == Direction::Next && relevant > offset + 1 {
2733                    Some(relevant)
2734                } else {
2735                    None
2736                }
2737            });
2738
2739            let offset = if direction == Direction::Prev {
2740                possibilities.max().unwrap_or(0)
2741            } else {
2742                possibilities.min().unwrap_or(map.buffer_snapshot.len())
2743            };
2744
2745            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2746            if new_point == display_point {
2747                break;
2748            }
2749            display_point = new_point;
2750        }
2751        return display_point;
2752    };
2753
2754    for _ in 0..times {
2755        let next_point = if is_start {
2756            movement::start_of_excerpt(map, display_point, direction)
2757        } else {
2758            movement::end_of_excerpt(map, display_point, direction)
2759        };
2760        if next_point == display_point {
2761            break;
2762        }
2763        display_point = next_point;
2764    }
2765
2766    display_point
2767}
2768
2769#[cfg(test)]
2770mod test {
2771
2772    use crate::{
2773        state::Mode,
2774        test::{NeovimBackedTestContext, VimTestContext},
2775    };
2776    use editor::display_map::Inlay;
2777    use indoc::indoc;
2778    use language::Point;
2779    use multi_buffer::MultiBufferRow;
2780
2781    #[gpui::test]
2782    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2783        let mut cx = NeovimBackedTestContext::new(cx).await;
2784
2785        let initial_state = indoc! {r"ˇabc
2786            def
2787
2788            paragraph
2789            the second
2790
2791
2792
2793            third and
2794            final"};
2795
2796        // goes down once
2797        cx.set_shared_state(initial_state).await;
2798        cx.simulate_shared_keystrokes("}").await;
2799        cx.shared_state().await.assert_eq(indoc! {r"abc
2800            def
2801            ˇ
2802            paragraph
2803            the second
2804
2805
2806
2807            third and
2808            final"});
2809
2810        // goes up once
2811        cx.simulate_shared_keystrokes("{").await;
2812        cx.shared_state().await.assert_eq(initial_state);
2813
2814        // goes down twice
2815        cx.simulate_shared_keystrokes("2 }").await;
2816        cx.shared_state().await.assert_eq(indoc! {r"abc
2817            def
2818
2819            paragraph
2820            the second
2821            ˇ
2822
2823
2824            third and
2825            final"});
2826
2827        // goes down over multiple blanks
2828        cx.simulate_shared_keystrokes("}").await;
2829        cx.shared_state().await.assert_eq(indoc! {r"abc
2830                def
2831
2832                paragraph
2833                the second
2834
2835
2836
2837                third and
2838                finaˇl"});
2839
2840        // goes up twice
2841        cx.simulate_shared_keystrokes("2 {").await;
2842        cx.shared_state().await.assert_eq(indoc! {r"abc
2843                def
2844                ˇ
2845                paragraph
2846                the second
2847
2848
2849
2850                third and
2851                final"});
2852    }
2853
2854    #[gpui::test]
2855    async fn test_matching(cx: &mut gpui::TestAppContext) {
2856        let mut cx = NeovimBackedTestContext::new(cx).await;
2857
2858        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2859                do(something(with<Types>.and_arrays[0, 2]))
2860            }"})
2861            .await;
2862        cx.simulate_shared_keystrokes("%").await;
2863        cx.shared_state()
2864            .await
2865            .assert_eq(indoc! {r"func (a stringˇ) {
2866                do(something(with<Types>.and_arrays[0, 2]))
2867            }"});
2868
2869        // test it works on the last character of the line
2870        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2871            do(something(with<Types>.and_arrays[0, 2]))
2872            }"})
2873            .await;
2874        cx.simulate_shared_keystrokes("%").await;
2875        cx.shared_state()
2876            .await
2877            .assert_eq(indoc! {r"func (a string) {
2878            do(something(with<Types>.and_arrays[0, 2]))
2879            ˇ}"});
2880
2881        // test it works on immediate nesting
2882        cx.set_shared_state("ˇ{()}").await;
2883        cx.simulate_shared_keystrokes("%").await;
2884        cx.shared_state().await.assert_eq("{()ˇ}");
2885        cx.simulate_shared_keystrokes("%").await;
2886        cx.shared_state().await.assert_eq("ˇ{()}");
2887
2888        // test it works on immediate nesting inside braces
2889        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2890        cx.simulate_shared_keystrokes("%").await;
2891        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2892
2893        // test it jumps to the next paren on a line
2894        cx.set_shared_state("func ˇboop() {\n}").await;
2895        cx.simulate_shared_keystrokes("%").await;
2896        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2897    }
2898
2899    #[gpui::test]
2900    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2901        let mut cx = NeovimBackedTestContext::new(cx).await;
2902
2903        // test it works with curly braces
2904        cx.set_shared_state(indoc! {r"func (a string) {
2905                do(something(with<Types>.anˇd_arrays[0, 2]))
2906            }"})
2907            .await;
2908        cx.simulate_shared_keystrokes("] }").await;
2909        cx.shared_state()
2910            .await
2911            .assert_eq(indoc! {r"func (a string) {
2912                do(something(with<Types>.and_arrays[0, 2]))
2913            ˇ}"});
2914
2915        // test it works with brackets
2916        cx.set_shared_state(indoc! {r"func (a string) {
2917                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2918            }"})
2919            .await;
2920        cx.simulate_shared_keystrokes("] )").await;
2921        cx.shared_state()
2922            .await
2923            .assert_eq(indoc! {r"func (a string) {
2924                do(something(with<Types>.and_arrays[0, 2])ˇ)
2925            }"});
2926
2927        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2928            .await;
2929        cx.simulate_shared_keystrokes("] )").await;
2930        cx.shared_state()
2931            .await
2932            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2933
2934        // test it works on immediate nesting
2935        cx.set_shared_state("{ˇ {}{}}").await;
2936        cx.simulate_shared_keystrokes("] }").await;
2937        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2938        cx.set_shared_state("(ˇ ()())").await;
2939        cx.simulate_shared_keystrokes("] )").await;
2940        cx.shared_state().await.assert_eq("( ()()ˇ)");
2941
2942        // test it works on immediate nesting inside braces
2943        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2944        cx.simulate_shared_keystrokes("] }").await;
2945        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2946        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2947        cx.simulate_shared_keystrokes("] )").await;
2948        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2949    }
2950
2951    #[gpui::test]
2952    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2953        let mut cx = NeovimBackedTestContext::new(cx).await;
2954
2955        // test it works with curly braces
2956        cx.set_shared_state(indoc! {r"func (a string) {
2957                do(something(with<Types>.anˇd_arrays[0, 2]))
2958            }"})
2959            .await;
2960        cx.simulate_shared_keystrokes("[ {").await;
2961        cx.shared_state()
2962            .await
2963            .assert_eq(indoc! {r"func (a string) ˇ{
2964                do(something(with<Types>.and_arrays[0, 2]))
2965            }"});
2966
2967        // test it works with brackets
2968        cx.set_shared_state(indoc! {r"func (a string) {
2969                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2970            }"})
2971            .await;
2972        cx.simulate_shared_keystrokes("[ (").await;
2973        cx.shared_state()
2974            .await
2975            .assert_eq(indoc! {r"func (a string) {
2976                doˇ(something(with<Types>.and_arrays[0, 2]))
2977            }"});
2978
2979        // test it works on immediate nesting
2980        cx.set_shared_state("{{}{} ˇ }").await;
2981        cx.simulate_shared_keystrokes("[ {").await;
2982        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2983        cx.set_shared_state("(()() ˇ )").await;
2984        cx.simulate_shared_keystrokes("[ (").await;
2985        cx.shared_state().await.assert_eq("ˇ(()()  )");
2986
2987        // test it works on immediate nesting inside braces
2988        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2989        cx.simulate_shared_keystrokes("[ {").await;
2990        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2991        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2992        cx.simulate_shared_keystrokes("[ (").await;
2993        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2994    }
2995
2996    #[gpui::test]
2997    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2998        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2999
3000        cx.neovim.exec("set filetype=html").await;
3001
3002        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3003        cx.simulate_shared_keystrokes("%").await;
3004        cx.shared_state()
3005            .await
3006            .assert_eq(indoc! {r"<body><ˇ/body>"});
3007        cx.simulate_shared_keystrokes("%").await;
3008
3009        // test jumping backwards
3010        cx.shared_state()
3011            .await
3012            .assert_eq(indoc! {r"<ˇbody></body>"});
3013
3014        // test self-closing tags
3015        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3016        cx.simulate_shared_keystrokes("%").await;
3017        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3018
3019        // test tag with attributes
3020        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3021            </div>
3022            "})
3023            .await;
3024        cx.simulate_shared_keystrokes("%").await;
3025        cx.shared_state()
3026            .await
3027            .assert_eq(indoc! {r"<div class='test' id='main'>
3028            <ˇ/div>
3029            "});
3030
3031        // test multi-line self-closing tag
3032        cx.set_shared_state(indoc! {r#"<a>
3033            <br
3034                test = "test"
3035            /ˇ>
3036        </a>"#})
3037            .await;
3038        cx.simulate_shared_keystrokes("%").await;
3039        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3040            ˇ<br
3041                test = "test"
3042            />
3043        </a>"#});
3044    }
3045
3046    #[gpui::test]
3047    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3048        let mut cx = NeovimBackedTestContext::new(cx).await;
3049
3050        // f and F
3051        cx.set_shared_state("ˇone two three four").await;
3052        cx.simulate_shared_keystrokes("f o").await;
3053        cx.shared_state().await.assert_eq("one twˇo three four");
3054        cx.simulate_shared_keystrokes(",").await;
3055        cx.shared_state().await.assert_eq("ˇone two three four");
3056        cx.simulate_shared_keystrokes("2 ;").await;
3057        cx.shared_state().await.assert_eq("one two three fˇour");
3058        cx.simulate_shared_keystrokes("shift-f e").await;
3059        cx.shared_state().await.assert_eq("one two threˇe four");
3060        cx.simulate_shared_keystrokes("2 ;").await;
3061        cx.shared_state().await.assert_eq("onˇe two three four");
3062        cx.simulate_shared_keystrokes(",").await;
3063        cx.shared_state().await.assert_eq("one two thrˇee four");
3064
3065        // t and T
3066        cx.set_shared_state("ˇone two three four").await;
3067        cx.simulate_shared_keystrokes("t o").await;
3068        cx.shared_state().await.assert_eq("one tˇwo three four");
3069        cx.simulate_shared_keystrokes(",").await;
3070        cx.shared_state().await.assert_eq("oˇne two three four");
3071        cx.simulate_shared_keystrokes("2 ;").await;
3072        cx.shared_state().await.assert_eq("one two three ˇfour");
3073        cx.simulate_shared_keystrokes("shift-t e").await;
3074        cx.shared_state().await.assert_eq("one two threeˇ four");
3075        cx.simulate_shared_keystrokes("3 ;").await;
3076        cx.shared_state().await.assert_eq("oneˇ two three four");
3077        cx.simulate_shared_keystrokes(",").await;
3078        cx.shared_state().await.assert_eq("one two thˇree four");
3079    }
3080
3081    #[gpui::test]
3082    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3083        let mut cx = NeovimBackedTestContext::new(cx).await;
3084        let initial_state = indoc! {r"something(ˇfoo)"};
3085        cx.set_shared_state(initial_state).await;
3086        cx.simulate_shared_keystrokes("}").await;
3087        cx.shared_state().await.assert_eq("something(fooˇ)");
3088    }
3089
3090    #[gpui::test]
3091    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3092        let mut cx = NeovimBackedTestContext::new(cx).await;
3093        cx.set_shared_state("ˇone\n  two\nthree").await;
3094        cx.simulate_shared_keystrokes("enter").await;
3095        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3096    }
3097
3098    #[gpui::test]
3099    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3100        let mut cx = NeovimBackedTestContext::new(cx).await;
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
3105        cx.set_shared_state("ˇ one \n two \nthree").await;
3106        cx.simulate_shared_keystrokes("g _").await;
3107        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3108        cx.simulate_shared_keystrokes("2 g _").await;
3109        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3110    }
3111
3112    #[gpui::test]
3113    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3114        let mut cx = NeovimBackedTestContext::new(cx).await;
3115        let initial_state = indoc! {r"abc
3116          def
3117          paragraph
3118          the second
3119          third ˇand
3120          final"};
3121
3122        cx.set_shared_state(initial_state).await;
3123        cx.simulate_shared_keystrokes("shift-h").await;
3124        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3125          def
3126          paragraph
3127          the second
3128          third and
3129          final"});
3130
3131        // clip point
3132        cx.set_shared_state(indoc! {r"
3133          1 2 3
3134          4 5 6
3135          7 8 ˇ9
3136          "})
3137            .await;
3138        cx.simulate_shared_keystrokes("shift-h").await;
3139        cx.shared_state().await.assert_eq(indoc! {"
3140          1 2 ˇ3
3141          4 5 6
3142          7 8 9
3143          "});
3144
3145        cx.set_shared_state(indoc! {r"
3146          1 2 3
3147          4 5 6
3148          ˇ7 8 9
3149          "})
3150            .await;
3151        cx.simulate_shared_keystrokes("shift-h").await;
3152        cx.shared_state().await.assert_eq(indoc! {"
3153          ˇ1 2 3
3154          4 5 6
3155          7 8 9
3156          "});
3157
3158        cx.set_shared_state(indoc! {r"
3159          1 2 3
3160          4 5 ˇ6
3161          7 8 9"})
3162            .await;
3163        cx.simulate_shared_keystrokes("9 shift-h").await;
3164        cx.shared_state().await.assert_eq(indoc! {"
3165          1 2 3
3166          4 5 6
3167          7 8 ˇ9"});
3168    }
3169
3170    #[gpui::test]
3171    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3172        let mut cx = NeovimBackedTestContext::new(cx).await;
3173        let initial_state = indoc! {r"abˇc
3174          def
3175          paragraph
3176          the second
3177          third and
3178          final"};
3179
3180        cx.set_shared_state(initial_state).await;
3181        cx.simulate_shared_keystrokes("shift-m").await;
3182        cx.shared_state().await.assert_eq(indoc! {r"abc
3183          def
3184          paˇragraph
3185          the second
3186          third and
3187          final"});
3188
3189        cx.set_shared_state(indoc! {r"
3190          1 2 3
3191          4 5 6
3192          7 8 ˇ9
3193          "})
3194            .await;
3195        cx.simulate_shared_keystrokes("shift-m").await;
3196        cx.shared_state().await.assert_eq(indoc! {"
3197          1 2 3
3198          4 5 ˇ6
3199          7 8 9
3200          "});
3201        cx.set_shared_state(indoc! {r"
3202          1 2 3
3203          4 5 6
3204          ˇ7 8 9
3205          "})
3206            .await;
3207        cx.simulate_shared_keystrokes("shift-m").await;
3208        cx.shared_state().await.assert_eq(indoc! {"
3209          1 2 3
3210          ˇ4 5 6
3211          7 8 9
3212          "});
3213        cx.set_shared_state(indoc! {r"
3214          ˇ1 2 3
3215          4 5 6
3216          7 8 9
3217          "})
3218            .await;
3219        cx.simulate_shared_keystrokes("shift-m").await;
3220        cx.shared_state().await.assert_eq(indoc! {"
3221          1 2 3
3222          ˇ4 5 6
3223          7 8 9
3224          "});
3225        cx.set_shared_state(indoc! {r"
3226          1 2 3
3227          ˇ4 5 6
3228          7 8 9
3229          "})
3230            .await;
3231        cx.simulate_shared_keystrokes("shift-m").await;
3232        cx.shared_state().await.assert_eq(indoc! {"
3233          1 2 3
3234          ˇ4 5 6
3235          7 8 9
3236          "});
3237        cx.set_shared_state(indoc! {r"
3238          1 2 3
3239          4 5 ˇ6
3240          7 8 9
3241          "})
3242            .await;
3243        cx.simulate_shared_keystrokes("shift-m").await;
3244        cx.shared_state().await.assert_eq(indoc! {"
3245          1 2 3
3246          4 5 ˇ6
3247          7 8 9
3248          "});
3249    }
3250
3251    #[gpui::test]
3252    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3253        let mut cx = NeovimBackedTestContext::new(cx).await;
3254        let initial_state = indoc! {r"abc
3255          deˇf
3256          paragraph
3257          the second
3258          third and
3259          final"};
3260
3261        cx.set_shared_state(initial_state).await;
3262        cx.simulate_shared_keystrokes("shift-l").await;
3263        cx.shared_state().await.assert_eq(indoc! {r"abc
3264          def
3265          paragraph
3266          the second
3267          third and
3268          fiˇnal"});
3269
3270        cx.set_shared_state(indoc! {r"
3271          1 2 3
3272          4 5 ˇ6
3273          7 8 9
3274          "})
3275            .await;
3276        cx.simulate_shared_keystrokes("shift-l").await;
3277        cx.shared_state().await.assert_eq(indoc! {"
3278          1 2 3
3279          4 5 6
3280          7 8 9
3281          ˇ"});
3282
3283        cx.set_shared_state(indoc! {r"
3284          1 2 3
3285          ˇ4 5 6
3286          7 8 9
3287          "})
3288            .await;
3289        cx.simulate_shared_keystrokes("shift-l").await;
3290        cx.shared_state().await.assert_eq(indoc! {"
3291          1 2 3
3292          4 5 6
3293          7 8 9
3294          ˇ"});
3295
3296        cx.set_shared_state(indoc! {r"
3297          1 2 ˇ3
3298          4 5 6
3299          7 8 9
3300          "})
3301            .await;
3302        cx.simulate_shared_keystrokes("shift-l").await;
3303        cx.shared_state().await.assert_eq(indoc! {"
3304          1 2 3
3305          4 5 6
3306          7 8 9
3307          ˇ"});
3308
3309        cx.set_shared_state(indoc! {r"
3310          ˇ1 2 3
3311          4 5 6
3312          7 8 9
3313          "})
3314            .await;
3315        cx.simulate_shared_keystrokes("shift-l").await;
3316        cx.shared_state().await.assert_eq(indoc! {"
3317          1 2 3
3318          4 5 6
3319          7 8 9
3320          ˇ"});
3321
3322        cx.set_shared_state(indoc! {r"
3323          1 2 3
3324          4 5 ˇ6
3325          7 8 9
3326          "})
3327            .await;
3328        cx.simulate_shared_keystrokes("9 shift-l").await;
3329        cx.shared_state().await.assert_eq(indoc! {"
3330          1 2 ˇ3
3331          4 5 6
3332          7 8 9
3333          "});
3334    }
3335
3336    #[gpui::test]
3337    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3338        let mut cx = NeovimBackedTestContext::new(cx).await;
3339        cx.set_shared_state(indoc! {r"
3340        456 5ˇ67 678
3341        "})
3342            .await;
3343        cx.simulate_shared_keystrokes("g e").await;
3344        cx.shared_state().await.assert_eq(indoc! {"
3345        45ˇ6 567 678
3346        "});
3347
3348        // Test times
3349        cx.set_shared_state(indoc! {r"
3350        123 234 345
3351        456 5ˇ67 678
3352        "})
3353            .await;
3354        cx.simulate_shared_keystrokes("4 g e").await;
3355        cx.shared_state().await.assert_eq(indoc! {"
3356        12ˇ3 234 345
3357        456 567 678
3358        "});
3359
3360        // With punctuation
3361        cx.set_shared_state(indoc! {r"
3362        123 234 345
3363        4;5.6 5ˇ67 678
3364        789 890 901
3365        "})
3366            .await;
3367        cx.simulate_shared_keystrokes("g e").await;
3368        cx.shared_state().await.assert_eq(indoc! {"
3369          123 234 345
3370          4;5.ˇ6 567 678
3371          789 890 901
3372        "});
3373
3374        // With punctuation and count
3375        cx.set_shared_state(indoc! {r"
3376        123 234 345
3377        4;5.6 5ˇ67 678
3378        789 890 901
3379        "})
3380            .await;
3381        cx.simulate_shared_keystrokes("5 g e").await;
3382        cx.shared_state().await.assert_eq(indoc! {"
3383          123 234 345
3384          ˇ4;5.6 567 678
3385          789 890 901
3386        "});
3387
3388        // newlines
3389        cx.set_shared_state(indoc! {r"
3390        123 234 345
3391
3392        78ˇ9 890 901
3393        "})
3394            .await;
3395        cx.simulate_shared_keystrokes("g e").await;
3396        cx.shared_state().await.assert_eq(indoc! {"
3397          123 234 345
3398          ˇ
3399          789 890 901
3400        "});
3401        cx.simulate_shared_keystrokes("g e").await;
3402        cx.shared_state().await.assert_eq(indoc! {"
3403          123 234 34ˇ5
3404
3405          789 890 901
3406        "});
3407
3408        // With punctuation
3409        cx.set_shared_state(indoc! {r"
3410        123 234 345
3411        4;5.ˇ6 567 678
3412        789 890 901
3413        "})
3414            .await;
3415        cx.simulate_shared_keystrokes("g shift-e").await;
3416        cx.shared_state().await.assert_eq(indoc! {"
3417          123 234 34ˇ5
3418          4;5.6 567 678
3419          789 890 901
3420        "});
3421    }
3422
3423    #[gpui::test]
3424    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3425        let mut cx = NeovimBackedTestContext::new(cx).await;
3426
3427        cx.set_shared_state(indoc! {"
3428            fn aˇ() {
3429              return
3430            }
3431        "})
3432            .await;
3433        cx.simulate_shared_keystrokes("v $ %").await;
3434        cx.shared_state().await.assert_eq(indoc! {"
3435            fn a«() {
3436              return
3437            }ˇ»
3438        "});
3439    }
3440
3441    #[gpui::test]
3442    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3443        let mut cx = VimTestContext::new(cx, true).await;
3444
3445        cx.set_state(
3446            indoc! {"
3447                struct Foo {
3448                ˇ
3449                }
3450            "},
3451            Mode::Normal,
3452        );
3453
3454        cx.update_editor(|editor, _window, cx| {
3455            let range = editor.selections.newest_anchor().range();
3456            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3457            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3458            editor.splice_inlays(&[], vec![inlay], cx);
3459        });
3460
3461        cx.simulate_keystrokes("j");
3462        cx.assert_state(
3463            indoc! {"
3464                struct Foo {
3465
3466                ˇ}
3467            "},
3468            Mode::Normal,
3469        );
3470    }
3471
3472    #[gpui::test]
3473    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3474        let mut cx = VimTestContext::new(cx, true).await;
3475
3476        cx.set_state(
3477            indoc! {"
3478            ˇstruct Foo {
3479
3480            }
3481        "},
3482            Mode::Normal,
3483        );
3484        cx.update_editor(|editor, _window, cx| {
3485            let snapshot = editor.buffer().read(cx).snapshot(cx);
3486            let end_of_line =
3487                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3488            let inlay_text = " hint";
3489            let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3490            editor.splice_inlays(&[], vec![inlay], cx);
3491        });
3492        cx.simulate_keystrokes("$");
3493        cx.assert_state(
3494            indoc! {"
3495            struct Foo ˇ{
3496
3497            }
3498        "},
3499            Mode::Normal,
3500        );
3501    }
3502
3503    #[gpui::test]
3504    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3505        let mut cx = NeovimBackedTestContext::new(cx).await;
3506        // Normal mode
3507        cx.set_shared_state(indoc! {"
3508            The ˇquick brown
3509            fox jumps over
3510            the lazy dog
3511            The quick brown
3512            fox jumps over
3513            the lazy dog
3514            The quick brown
3515            fox jumps over
3516            the lazy dog"})
3517            .await;
3518        cx.simulate_shared_keystrokes("2 0 %").await;
3519        cx.shared_state().await.assert_eq(indoc! {"
3520            The quick brown
3521            fox ˇjumps over
3522            the lazy dog
3523            The quick brown
3524            fox jumps over
3525            the lazy dog
3526            The quick brown
3527            fox jumps over
3528            the lazy dog"});
3529
3530        cx.simulate_shared_keystrokes("2 5 %").await;
3531        cx.shared_state().await.assert_eq(indoc! {"
3532            The quick brown
3533            fox jumps over
3534            the ˇlazy dog
3535            The quick brown
3536            fox jumps over
3537            the lazy dog
3538            The quick brown
3539            fox jumps over
3540            the lazy dog"});
3541
3542        cx.simulate_shared_keystrokes("7 5 %").await;
3543        cx.shared_state().await.assert_eq(indoc! {"
3544            The quick brown
3545            fox jumps over
3546            the lazy dog
3547            The quick brown
3548            fox jumps over
3549            the lazy dog
3550            The ˇquick brown
3551            fox jumps over
3552            the lazy dog"});
3553
3554        // Visual mode
3555        cx.set_shared_state(indoc! {"
3556            The ˇquick brown
3557            fox jumps over
3558            the lazy dog
3559            The quick brown
3560            fox jumps over
3561            the lazy dog
3562            The quick brown
3563            fox jumps over
3564            the lazy dog"})
3565            .await;
3566        cx.simulate_shared_keystrokes("v 5 0 %").await;
3567        cx.shared_state().await.assert_eq(indoc! {"
3568            The «quick brown
3569            fox jumps over
3570            the lazy dog
3571            The quick brown
3572            fox jˇ»umps over
3573            the lazy dog
3574            The quick brown
3575            fox jumps over
3576            the lazy dog"});
3577
3578        cx.set_shared_state(indoc! {"
3579            The ˇquick brown
3580            fox jumps over
3581            the lazy dog
3582            The quick brown
3583            fox jumps over
3584            the lazy dog
3585            The quick brown
3586            fox jumps over
3587            the lazy dog"})
3588            .await;
3589        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3590        cx.shared_state().await.assert_eq(indoc! {"
3591            The «quick brown
3592            fox jumps over
3593            the lazy dog
3594            The quick brown
3595            fox jumps over
3596            the lazy dog
3597            The quick brown
3598            fox jumps over
3599            the lˇ»azy dog"});
3600    }
3601}