motion.rs

   1use editor::{
   2    display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
   3    movement::{
   4        self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
   5    },
   6    Bias, DisplayPoint, ToOffset,
   7};
   8use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
   9use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
  10use serde::Deserialize;
  11use std::ops::Range;
  12use workspace::Workspace;
  13
  14use crate::{
  15    normal::normal_motion,
  16    state::{Mode, Operator},
  17    surrounds::SurroundsType,
  18    utils::coerce_punctuation,
  19    visual::visual_motion,
  20    Vim,
  21};
  22
  23#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
  24pub enum Motion {
  25    Left,
  26    Backspace,
  27    Down {
  28        display_lines: bool,
  29    },
  30    Up {
  31        display_lines: bool,
  32    },
  33    Right,
  34    Space,
  35    NextWordStart {
  36        ignore_punctuation: bool,
  37    },
  38    NextWordEnd {
  39        ignore_punctuation: bool,
  40    },
  41    PreviousWordStart {
  42        ignore_punctuation: bool,
  43    },
  44    PreviousWordEnd {
  45        ignore_punctuation: bool,
  46    },
  47    NextSubwordStart {
  48        ignore_punctuation: bool,
  49    },
  50    NextSubwordEnd {
  51        ignore_punctuation: bool,
  52    },
  53    PreviousSubwordStart {
  54        ignore_punctuation: bool,
  55    },
  56    PreviousSubwordEnd {
  57        ignore_punctuation: bool,
  58    },
  59    FirstNonWhitespace {
  60        display_lines: bool,
  61    },
  62    CurrentLine,
  63    StartOfLine {
  64        display_lines: bool,
  65    },
  66    EndOfLine {
  67        display_lines: bool,
  68    },
  69    StartOfParagraph,
  70    EndOfParagraph,
  71    StartOfDocument,
  72    EndOfDocument,
  73    Matching,
  74    FindForward {
  75        before: bool,
  76        char: char,
  77        mode: FindRange,
  78        smartcase: bool,
  79    },
  80    FindBackward {
  81        after: bool,
  82        char: char,
  83        mode: FindRange,
  84        smartcase: bool,
  85    },
  86    RepeatFind {
  87        last_find: Box<Motion>,
  88    },
  89    RepeatFindReversed {
  90        last_find: Box<Motion>,
  91    },
  92    NextLineStart,
  93    StartOfLineDownward,
  94    EndOfLineDownward,
  95    GoToColumn,
  96    WindowTop,
  97    WindowMiddle,
  98    WindowBottom,
  99}
 100
 101#[derive(Clone, Deserialize, PartialEq)]
 102#[serde(rename_all = "camelCase")]
 103struct NextWordStart {
 104    #[serde(default)]
 105    ignore_punctuation: bool,
 106}
 107
 108#[derive(Clone, Deserialize, PartialEq)]
 109#[serde(rename_all = "camelCase")]
 110struct NextWordEnd {
 111    #[serde(default)]
 112    ignore_punctuation: bool,
 113}
 114
 115#[derive(Clone, Deserialize, PartialEq)]
 116#[serde(rename_all = "camelCase")]
 117struct PreviousWordStart {
 118    #[serde(default)]
 119    ignore_punctuation: bool,
 120}
 121
 122#[derive(Clone, Deserialize, PartialEq)]
 123#[serde(rename_all = "camelCase")]
 124struct PreviousWordEnd {
 125    #[serde(default)]
 126    ignore_punctuation: bool,
 127}
 128
 129#[derive(Clone, Deserialize, PartialEq)]
 130#[serde(rename_all = "camelCase")]
 131pub(crate) struct NextSubwordStart {
 132    #[serde(default)]
 133    pub(crate) ignore_punctuation: bool,
 134}
 135
 136#[derive(Clone, Deserialize, PartialEq)]
 137#[serde(rename_all = "camelCase")]
 138pub(crate) struct NextSubwordEnd {
 139    #[serde(default)]
 140    pub(crate) ignore_punctuation: bool,
 141}
 142
 143#[derive(Clone, Deserialize, PartialEq)]
 144#[serde(rename_all = "camelCase")]
 145pub(crate) struct PreviousSubwordStart {
 146    #[serde(default)]
 147    pub(crate) ignore_punctuation: bool,
 148}
 149
 150#[derive(Clone, Deserialize, PartialEq)]
 151#[serde(rename_all = "camelCase")]
 152pub(crate) struct PreviousSubwordEnd {
 153    #[serde(default)]
 154    pub(crate) ignore_punctuation: bool,
 155}
 156
 157#[derive(Clone, Deserialize, PartialEq)]
 158#[serde(rename_all = "camelCase")]
 159pub(crate) struct Up {
 160    #[serde(default)]
 161    pub(crate) display_lines: bool,
 162}
 163
 164#[derive(Clone, Deserialize, PartialEq)]
 165#[serde(rename_all = "camelCase")]
 166pub(crate) struct Down {
 167    #[serde(default)]
 168    pub(crate) display_lines: bool,
 169}
 170
 171#[derive(Clone, Deserialize, PartialEq)]
 172#[serde(rename_all = "camelCase")]
 173struct FirstNonWhitespace {
 174    #[serde(default)]
 175    display_lines: bool,
 176}
 177
 178#[derive(Clone, Deserialize, PartialEq)]
 179#[serde(rename_all = "camelCase")]
 180struct EndOfLine {
 181    #[serde(default)]
 182    display_lines: bool,
 183}
 184
 185#[derive(Clone, Deserialize, PartialEq)]
 186#[serde(rename_all = "camelCase")]
 187pub struct StartOfLine {
 188    #[serde(default)]
 189    pub(crate) display_lines: bool,
 190}
 191
 192impl_actions!(
 193    vim,
 194    [
 195        StartOfLine,
 196        EndOfLine,
 197        FirstNonWhitespace,
 198        Down,
 199        Up,
 200        NextWordStart,
 201        NextWordEnd,
 202        PreviousWordStart,
 203        PreviousWordEnd,
 204        NextSubwordStart,
 205        NextSubwordEnd,
 206        PreviousSubwordStart,
 207        PreviousSubwordEnd,
 208    ]
 209);
 210
 211actions!(
 212    vim,
 213    [
 214        Left,
 215        Backspace,
 216        Right,
 217        Space,
 218        CurrentLine,
 219        StartOfParagraph,
 220        EndOfParagraph,
 221        StartOfDocument,
 222        EndOfDocument,
 223        Matching,
 224        NextLineStart,
 225        StartOfLineDownward,
 226        EndOfLineDownward,
 227        GoToColumn,
 228        RepeatFind,
 229        RepeatFindReversed,
 230        WindowTop,
 231        WindowMiddle,
 232        WindowBottom,
 233    ]
 234);
 235
 236pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 237    workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 238    workspace
 239        .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
 240    workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
 241        motion(
 242            Motion::Down {
 243                display_lines: action.display_lines,
 244            },
 245            cx,
 246        )
 247    });
 248    workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
 249        motion(
 250            Motion::Up {
 251                display_lines: action.display_lines,
 252            },
 253            cx,
 254        )
 255    });
 256    workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 257    workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx));
 258    workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
 259        motion(
 260            Motion::FirstNonWhitespace {
 261                display_lines: action.display_lines,
 262            },
 263            cx,
 264        )
 265    });
 266    workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
 267        motion(
 268            Motion::StartOfLine {
 269                display_lines: action.display_lines,
 270            },
 271            cx,
 272        )
 273    });
 274    workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
 275        motion(
 276            Motion::EndOfLine {
 277                display_lines: action.display_lines,
 278            },
 279            cx,
 280        )
 281    });
 282    workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
 283        motion(Motion::CurrentLine, cx)
 284    });
 285    workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
 286        motion(Motion::StartOfParagraph, cx)
 287    });
 288    workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
 289        motion(Motion::EndOfParagraph, cx)
 290    });
 291    workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
 292        motion(Motion::StartOfDocument, cx)
 293    });
 294    workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
 295        motion(Motion::EndOfDocument, cx)
 296    });
 297    workspace
 298        .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
 299
 300    workspace.register_action(
 301        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
 302            motion(Motion::NextWordStart { ignore_punctuation }, cx)
 303        },
 304    );
 305    workspace.register_action(
 306        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
 307            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
 308        },
 309    );
 310    workspace.register_action(
 311        |_: &mut Workspace,
 312         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
 313         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
 314    );
 315    workspace.register_action(
 316        |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
 317            motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
 318        },
 319    );
 320    workspace.register_action(
 321        |_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| {
 322            motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
 323        },
 324    );
 325    workspace.register_action(
 326        |_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| {
 327            motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
 328        },
 329    );
 330    workspace.register_action(
 331        |_: &mut Workspace,
 332         &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart,
 333         cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) },
 334    );
 335    workspace.register_action(
 336        |_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| {
 337            motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
 338        },
 339    );
 340    workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
 341        motion(Motion::NextLineStart, cx)
 342    });
 343    workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
 344        motion(Motion::StartOfLineDownward, cx)
 345    });
 346    workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
 347        motion(Motion::EndOfLineDownward, cx)
 348    });
 349    workspace
 350        .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
 351
 352    workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
 353        if let Some(last_find) = Vim::read(cx)
 354            .workspace_state
 355            .last_find
 356            .clone()
 357            .map(Box::new)
 358        {
 359            motion(Motion::RepeatFind { last_find }, cx);
 360        }
 361    });
 362
 363    workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
 364        if let Some(last_find) = Vim::read(cx)
 365            .workspace_state
 366            .last_find
 367            .clone()
 368            .map(Box::new)
 369        {
 370            motion(Motion::RepeatFindReversed { last_find }, cx);
 371        }
 372    });
 373    workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
 374    workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
 375        motion(Motion::WindowMiddle, cx)
 376    });
 377    workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
 378        motion(Motion::WindowBottom, cx)
 379    });
 380}
 381
 382pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 383    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
 384        Vim::read(cx).active_operator()
 385    {
 386        Vim::update(cx, |vim, cx| vim.pop_operator(cx));
 387    }
 388
 389    let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
 390    let active_operator = Vim::read(cx).active_operator();
 391    let mut waiting_operator: Option<Operator> = None;
 392    match Vim::read(cx).state().mode {
 393        Mode::Normal | Mode::Replace => {
 394            if active_operator == Some(Operator::AddSurrounds { target: None }) {
 395                waiting_operator = Some(Operator::AddSurrounds {
 396                    target: Some(SurroundsType::Motion(motion)),
 397                });
 398            } else {
 399                normal_motion(motion.clone(), active_operator.clone(), count, cx)
 400            }
 401        }
 402        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 403            visual_motion(motion.clone(), count, cx)
 404        }
 405        Mode::Insert => {
 406            // Shouldn't execute a motion in insert mode. Ignoring
 407        }
 408    }
 409    Vim::update(cx, |vim, cx| {
 410        vim.clear_operator(cx);
 411        if let Some(operator) = waiting_operator {
 412            vim.push_operator(operator, cx);
 413        }
 414    });
 415}
 416
 417// Motion handling is specified here:
 418// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 419impl Motion {
 420    pub fn linewise(&self) -> bool {
 421        use Motion::*;
 422        match self {
 423            Down { .. }
 424            | Up { .. }
 425            | StartOfDocument
 426            | EndOfDocument
 427            | CurrentLine
 428            | NextLineStart
 429            | StartOfLineDownward
 430            | StartOfParagraph
 431            | WindowTop
 432            | WindowMiddle
 433            | WindowBottom
 434            | EndOfParagraph => true,
 435            EndOfLine { .. }
 436            | Matching
 437            | FindForward { .. }
 438            | Left
 439            | Backspace
 440            | Right
 441            | Space
 442            | StartOfLine { .. }
 443            | EndOfLineDownward
 444            | GoToColumn
 445            | NextWordStart { .. }
 446            | NextWordEnd { .. }
 447            | PreviousWordStart { .. }
 448            | PreviousWordEnd { .. }
 449            | NextSubwordStart { .. }
 450            | NextSubwordEnd { .. }
 451            | PreviousSubwordStart { .. }
 452            | PreviousSubwordEnd { .. }
 453            | FirstNonWhitespace { .. }
 454            | FindBackward { .. }
 455            | RepeatFind { .. }
 456            | RepeatFindReversed { .. } => false,
 457        }
 458    }
 459
 460    pub fn infallible(&self) -> bool {
 461        use Motion::*;
 462        match self {
 463            StartOfDocument | EndOfDocument | CurrentLine => true,
 464            Down { .. }
 465            | Up { .. }
 466            | EndOfLine { .. }
 467            | Matching
 468            | FindForward { .. }
 469            | RepeatFind { .. }
 470            | Left
 471            | Backspace
 472            | Right
 473            | Space
 474            | StartOfLine { .. }
 475            | StartOfParagraph
 476            | EndOfParagraph
 477            | StartOfLineDownward
 478            | EndOfLineDownward
 479            | GoToColumn
 480            | NextWordStart { .. }
 481            | NextWordEnd { .. }
 482            | PreviousWordStart { .. }
 483            | PreviousWordEnd { .. }
 484            | NextSubwordStart { .. }
 485            | NextSubwordEnd { .. }
 486            | PreviousSubwordStart { .. }
 487            | PreviousSubwordEnd { .. }
 488            | FirstNonWhitespace { .. }
 489            | FindBackward { .. }
 490            | RepeatFindReversed { .. }
 491            | WindowTop
 492            | WindowMiddle
 493            | WindowBottom
 494            | NextLineStart => false,
 495        }
 496    }
 497
 498    pub fn inclusive(&self) -> bool {
 499        use Motion::*;
 500        match self {
 501            Down { .. }
 502            | Up { .. }
 503            | StartOfDocument
 504            | EndOfDocument
 505            | CurrentLine
 506            | EndOfLine { .. }
 507            | EndOfLineDownward
 508            | Matching
 509            | FindForward { .. }
 510            | WindowTop
 511            | WindowMiddle
 512            | WindowBottom
 513            | NextWordEnd { .. }
 514            | PreviousWordEnd { .. }
 515            | NextSubwordEnd { .. }
 516            | PreviousSubwordEnd { .. }
 517            | NextLineStart => true,
 518            Left
 519            | Backspace
 520            | Right
 521            | Space
 522            | StartOfLine { .. }
 523            | StartOfLineDownward
 524            | StartOfParagraph
 525            | EndOfParagraph
 526            | GoToColumn
 527            | NextWordStart { .. }
 528            | PreviousWordStart { .. }
 529            | NextSubwordStart { .. }
 530            | PreviousSubwordStart { .. }
 531            | FirstNonWhitespace { .. }
 532            | FindBackward { .. } => false,
 533            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 534                motion.inclusive()
 535            }
 536        }
 537    }
 538
 539    pub fn move_point(
 540        &self,
 541        map: &DisplaySnapshot,
 542        point: DisplayPoint,
 543        goal: SelectionGoal,
 544        maybe_times: Option<usize>,
 545        text_layout_details: &TextLayoutDetails,
 546    ) -> Option<(DisplayPoint, SelectionGoal)> {
 547        let times = maybe_times.unwrap_or(1);
 548        use Motion::*;
 549        let infallible = self.infallible();
 550        let (new_point, goal) = match self {
 551            Left => (left(map, point, times), SelectionGoal::None),
 552            Backspace => (backspace(map, point, times), SelectionGoal::None),
 553            Down {
 554                display_lines: false,
 555            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
 556            Down {
 557                display_lines: true,
 558            } => down_display(map, point, goal, times, &text_layout_details),
 559            Up {
 560                display_lines: false,
 561            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
 562            Up {
 563                display_lines: true,
 564            } => up_display(map, point, goal, times, &text_layout_details),
 565            Right => (right(map, point, times), SelectionGoal::None),
 566            Space => (space(map, point, times), SelectionGoal::None),
 567            NextWordStart { ignore_punctuation } => (
 568                next_word_start(map, point, *ignore_punctuation, times),
 569                SelectionGoal::None,
 570            ),
 571            NextWordEnd { ignore_punctuation } => (
 572                next_word_end(map, point, *ignore_punctuation, times, true),
 573                SelectionGoal::None,
 574            ),
 575            PreviousWordStart { ignore_punctuation } => (
 576                previous_word_start(map, point, *ignore_punctuation, times),
 577                SelectionGoal::None,
 578            ),
 579            PreviousWordEnd { ignore_punctuation } => (
 580                previous_word_end(map, point, *ignore_punctuation, times),
 581                SelectionGoal::None,
 582            ),
 583            NextSubwordStart { ignore_punctuation } => (
 584                next_subword_start(map, point, *ignore_punctuation, times),
 585                SelectionGoal::None,
 586            ),
 587            NextSubwordEnd { ignore_punctuation } => (
 588                next_subword_end(map, point, *ignore_punctuation, times, true),
 589                SelectionGoal::None,
 590            ),
 591            PreviousSubwordStart { ignore_punctuation } => (
 592                previous_subword_start(map, point, *ignore_punctuation, times),
 593                SelectionGoal::None,
 594            ),
 595            PreviousSubwordEnd { ignore_punctuation } => (
 596                previous_subword_end(map, point, *ignore_punctuation, times),
 597                SelectionGoal::None,
 598            ),
 599            FirstNonWhitespace { display_lines } => (
 600                first_non_whitespace(map, *display_lines, point),
 601                SelectionGoal::None,
 602            ),
 603            StartOfLine { display_lines } => (
 604                start_of_line(map, *display_lines, point),
 605                SelectionGoal::None,
 606            ),
 607            EndOfLine { display_lines } => (
 608                end_of_line(map, *display_lines, point, times),
 609                SelectionGoal::None,
 610            ),
 611            StartOfParagraph => (
 612                movement::start_of_paragraph(map, point, times),
 613                SelectionGoal::None,
 614            ),
 615            EndOfParagraph => (
 616                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 617                SelectionGoal::None,
 618            ),
 619            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 620            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 621            EndOfDocument => (
 622                end_of_document(map, point, maybe_times),
 623                SelectionGoal::None,
 624            ),
 625            Matching => (matching(map, point), SelectionGoal::None),
 626            // t f
 627            FindForward {
 628                before,
 629                char,
 630                mode,
 631                smartcase,
 632            } => {
 633                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
 634                    .map(|new_point| (new_point, SelectionGoal::None))
 635            }
 636            // T F
 637            FindBackward {
 638                after,
 639                char,
 640                mode,
 641                smartcase,
 642            } => (
 643                find_backward(map, point, *after, *char, times, *mode, *smartcase),
 644                SelectionGoal::None,
 645            ),
 646            // ; -- repeat the last find done with t, f, T, F
 647            RepeatFind { last_find } => match **last_find {
 648                Motion::FindForward {
 649                    before,
 650                    char,
 651                    mode,
 652                    smartcase,
 653                } => {
 654                    let mut new_point =
 655                        find_forward(map, point, before, char, times, mode, smartcase);
 656                    if new_point == Some(point) {
 657                        new_point =
 658                            find_forward(map, point, before, char, times + 1, mode, smartcase);
 659                    }
 660
 661                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 662                }
 663
 664                Motion::FindBackward {
 665                    after,
 666                    char,
 667                    mode,
 668                    smartcase,
 669                } => {
 670                    let mut new_point =
 671                        find_backward(map, point, after, char, times, mode, smartcase);
 672                    if new_point == point {
 673                        new_point =
 674                            find_backward(map, point, after, char, times + 1, mode, smartcase);
 675                    }
 676
 677                    (new_point, SelectionGoal::None)
 678                }
 679                _ => return None,
 680            },
 681            // , -- repeat the last find done with t, f, T, F, in opposite direction
 682            RepeatFindReversed { last_find } => match **last_find {
 683                Motion::FindForward {
 684                    before,
 685                    char,
 686                    mode,
 687                    smartcase,
 688                } => {
 689                    let mut new_point =
 690                        find_backward(map, point, before, char, times, mode, smartcase);
 691                    if new_point == point {
 692                        new_point =
 693                            find_backward(map, point, before, char, times + 1, mode, smartcase);
 694                    }
 695
 696                    (new_point, SelectionGoal::None)
 697                }
 698
 699                Motion::FindBackward {
 700                    after,
 701                    char,
 702                    mode,
 703                    smartcase,
 704                } => {
 705                    let mut new_point =
 706                        find_forward(map, point, after, char, times, mode, smartcase);
 707                    if new_point == Some(point) {
 708                        new_point =
 709                            find_forward(map, point, after, char, times + 1, mode, smartcase);
 710                    }
 711
 712                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 713                }
 714                _ => return None,
 715            },
 716            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 717            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 718            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
 719            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
 720            WindowTop => window_top(map, point, &text_layout_details, times - 1),
 721            WindowMiddle => window_middle(map, point, &text_layout_details),
 722            WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
 723        };
 724
 725        (new_point != point || infallible).then_some((new_point, goal))
 726    }
 727
 728    // Get the range value after self is applied to the specified selection.
 729    pub fn range(
 730        &self,
 731        map: &DisplaySnapshot,
 732        selection: Selection<DisplayPoint>,
 733        times: Option<usize>,
 734        expand_to_surrounding_newline: bool,
 735        text_layout_details: &TextLayoutDetails,
 736    ) -> Option<Range<DisplayPoint>> {
 737        if let Some((new_head, goal)) = self.move_point(
 738            map,
 739            selection.head(),
 740            selection.goal,
 741            times,
 742            &text_layout_details,
 743        ) {
 744            let mut selection = selection.clone();
 745            selection.set_head(new_head, goal);
 746
 747            if self.linewise() {
 748                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 749
 750                if expand_to_surrounding_newline {
 751                    if selection.end.row() < map.max_point().row() {
 752                        *selection.end.row_mut() += 1;
 753                        *selection.end.column_mut() = 0;
 754                        selection.end = map.clip_point(selection.end, Bias::Right);
 755                        // Don't reset the end here
 756                        return Some(selection.start..selection.end);
 757                    } else if selection.start.row() > 0 {
 758                        *selection.start.row_mut() -= 1;
 759                        *selection.start.column_mut() = map.line_len(selection.start.row());
 760                        selection.start = map.clip_point(selection.start, Bias::Left);
 761                    }
 762                }
 763
 764                selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
 765            } else {
 766                // Another special case: When using the "w" motion in combination with an
 767                // operator and the last word moved over is at the end of a line, the end of
 768                // that word becomes the end of the operated text, not the first word in the
 769                // next line.
 770                if let Motion::NextWordStart {
 771                    ignore_punctuation: _,
 772                } = self
 773                {
 774                    let start_row = selection.start.to_point(&map).row;
 775                    if selection.end.to_point(&map).row > start_row {
 776                        selection.end =
 777                            Point::new(start_row, map.buffer_snapshot.line_len(start_row))
 778                                .to_display_point(&map)
 779                    }
 780                }
 781
 782                // If the motion is exclusive and the end of the motion is in column 1, the
 783                // end of the motion is moved to the end of the previous line and the motion
 784                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
 785                // but "d}" will not include that line.
 786                let mut inclusive = self.inclusive();
 787                if !inclusive
 788                    && self != &Motion::Backspace
 789                    && selection.end.row() > selection.start.row()
 790                    && selection.end.column() == 0
 791                {
 792                    inclusive = true;
 793                    *selection.end.row_mut() -= 1;
 794                    *selection.end.column_mut() = 0;
 795                    selection.end = map.clip_point(
 796                        map.next_line_boundary(selection.end.to_point(map)).1,
 797                        Bias::Left,
 798                    );
 799                }
 800
 801                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 802                    *selection.end.column_mut() += 1;
 803                }
 804            }
 805            Some(selection.start..selection.end)
 806        } else {
 807            None
 808        }
 809    }
 810
 811    // Expands a selection using self for an operator
 812    pub fn expand_selection(
 813        &self,
 814        map: &DisplaySnapshot,
 815        selection: &mut Selection<DisplayPoint>,
 816        times: Option<usize>,
 817        expand_to_surrounding_newline: bool,
 818        text_layout_details: &TextLayoutDetails,
 819    ) -> bool {
 820        if let Some(range) = self.range(
 821            map,
 822            selection.clone(),
 823            times,
 824            expand_to_surrounding_newline,
 825            text_layout_details,
 826        ) {
 827            selection.start = range.start;
 828            selection.end = range.end;
 829            true
 830        } else {
 831            false
 832        }
 833    }
 834}
 835
 836fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 837    for _ in 0..times {
 838        point = movement::saturating_left(map, point);
 839        if point.column() == 0 {
 840            break;
 841        }
 842    }
 843    point
 844}
 845
 846pub(crate) fn backspace(
 847    map: &DisplaySnapshot,
 848    mut point: DisplayPoint,
 849    times: usize,
 850) -> DisplayPoint {
 851    for _ in 0..times {
 852        point = movement::left(map, point);
 853        if point.is_zero() {
 854            break;
 855        }
 856    }
 857    point
 858}
 859
 860fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 861    for _ in 0..times {
 862        point = wrapping_right(map, point);
 863        if point == map.max_point() {
 864            break;
 865        }
 866    }
 867    point
 868}
 869
 870fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 871    let max_column = map.line_len(point.row()).saturating_sub(1);
 872    if point.column() < max_column {
 873        *point.column_mut() += 1;
 874    } else if point.row() < map.max_point().row() {
 875        *point.row_mut() += 1;
 876        *point.column_mut() = 0;
 877    }
 878    point
 879}
 880
 881pub(crate) fn start_of_relative_buffer_row(
 882    map: &DisplaySnapshot,
 883    point: DisplayPoint,
 884    times: isize,
 885) -> DisplayPoint {
 886    let start = map.display_point_to_fold_point(point, Bias::Left);
 887    let target = start.row() as isize + times;
 888    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 889
 890    map.clip_point(
 891        map.fold_point_to_display_point(
 892            map.fold_snapshot
 893                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 894        ),
 895        Bias::Right,
 896    )
 897}
 898
 899fn up_down_buffer_rows(
 900    map: &DisplaySnapshot,
 901    point: DisplayPoint,
 902    mut goal: SelectionGoal,
 903    times: isize,
 904    text_layout_details: &TextLayoutDetails,
 905) -> (DisplayPoint, SelectionGoal) {
 906    let start = map.display_point_to_fold_point(point, Bias::Left);
 907    let begin_folded_line = map.fold_point_to_display_point(
 908        map.fold_snapshot
 909            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
 910    );
 911    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 912
 913    let (goal_wrap, goal_x) = match goal {
 914        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
 915        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 916        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 917        _ => {
 918            let x = map.x_for_display_point(point, text_layout_details);
 919            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
 920            (select_nth_wrapped_row, x.0)
 921        }
 922    };
 923
 924    let target = start.row() as isize + times;
 925    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 926
 927    let mut begin_folded_line = map.fold_point_to_display_point(
 928        map.fold_snapshot
 929            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
 930    );
 931
 932    let mut i = 0;
 933    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
 934        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
 935        if map
 936            .display_point_to_fold_point(next_folded_line, Bias::Right)
 937            .row()
 938            == new_row
 939        {
 940            i += 1;
 941            begin_folded_line = next_folded_line;
 942        } else {
 943            break;
 944        }
 945    }
 946
 947    let new_col = if i == goal_wrap {
 948        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
 949    } else {
 950        map.line_len(begin_folded_line.row())
 951    };
 952
 953    (
 954        map.clip_point(
 955            DisplayPoint::new(begin_folded_line.row(), new_col),
 956            Bias::Left,
 957        ),
 958        goal,
 959    )
 960}
 961
 962fn down_display(
 963    map: &DisplaySnapshot,
 964    mut point: DisplayPoint,
 965    mut goal: SelectionGoal,
 966    times: usize,
 967    text_layout_details: &TextLayoutDetails,
 968) -> (DisplayPoint, SelectionGoal) {
 969    for _ in 0..times {
 970        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
 971    }
 972
 973    (point, goal)
 974}
 975
 976fn up_display(
 977    map: &DisplaySnapshot,
 978    mut point: DisplayPoint,
 979    mut goal: SelectionGoal,
 980    times: usize,
 981    text_layout_details: &TextLayoutDetails,
 982) -> (DisplayPoint, SelectionGoal) {
 983    for _ in 0..times {
 984        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
 985    }
 986
 987    (point, goal)
 988}
 989
 990pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 991    for _ in 0..times {
 992        let new_point = movement::saturating_right(map, point);
 993        if point == new_point {
 994            break;
 995        }
 996        point = new_point;
 997    }
 998    point
 999}
1000
1001pub(crate) fn next_char(
1002    map: &DisplaySnapshot,
1003    point: DisplayPoint,
1004    allow_cross_newline: bool,
1005) -> DisplayPoint {
1006    let mut new_point = point;
1007    let mut max_column = map.line_len(new_point.row());
1008    if !allow_cross_newline {
1009        max_column -= 1;
1010    }
1011    if new_point.column() < max_column {
1012        *new_point.column_mut() += 1;
1013    } else if new_point < map.max_point() && allow_cross_newline {
1014        *new_point.row_mut() += 1;
1015        *new_point.column_mut() = 0;
1016    }
1017    new_point
1018}
1019
1020pub(crate) fn next_word_start(
1021    map: &DisplaySnapshot,
1022    mut point: DisplayPoint,
1023    ignore_punctuation: bool,
1024    times: usize,
1025) -> DisplayPoint {
1026    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1027    for _ in 0..times {
1028        let mut crossed_newline = false;
1029        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1030            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1031            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1032            let at_newline = right == '\n';
1033
1034            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1035                || at_newline && crossed_newline
1036                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1037
1038            crossed_newline |= at_newline;
1039            found
1040        });
1041        if point == new_point {
1042            break;
1043        }
1044        point = new_point;
1045    }
1046    point
1047}
1048
1049pub(crate) fn next_word_end(
1050    map: &DisplaySnapshot,
1051    mut point: DisplayPoint,
1052    ignore_punctuation: bool,
1053    times: usize,
1054    allow_cross_newline: bool,
1055) -> DisplayPoint {
1056    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1057    for _ in 0..times {
1058        let new_point = next_char(map, point, allow_cross_newline);
1059        let mut need_next_char = false;
1060        let new_point = movement::find_boundary_exclusive(
1061            map,
1062            new_point,
1063            FindRange::MultiLine,
1064            |left, right| {
1065                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1066                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1067                let at_newline = right == '\n';
1068
1069                if !allow_cross_newline && at_newline {
1070                    need_next_char = true;
1071                    return true;
1072                }
1073
1074                left_kind != right_kind && left_kind != CharKind::Whitespace
1075            },
1076        );
1077        let new_point = if need_next_char {
1078            next_char(map, new_point, true)
1079        } else {
1080            new_point
1081        };
1082        let new_point = map.clip_point(new_point, Bias::Left);
1083        if point == new_point {
1084            break;
1085        }
1086        point = new_point;
1087    }
1088    point
1089}
1090
1091fn previous_word_start(
1092    map: &DisplaySnapshot,
1093    mut point: DisplayPoint,
1094    ignore_punctuation: bool,
1095    times: usize,
1096) -> DisplayPoint {
1097    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1098    for _ in 0..times {
1099        // This works even though find_preceding_boundary is called for every character in the line containing
1100        // cursor because the newline is checked only once.
1101        let new_point = movement::find_preceding_boundary_display_point(
1102            map,
1103            point,
1104            FindRange::MultiLine,
1105            |left, right| {
1106                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1107                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1108
1109                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1110            },
1111        );
1112        if point == new_point {
1113            break;
1114        }
1115        point = new_point;
1116    }
1117    point
1118}
1119
1120fn previous_word_end(
1121    map: &DisplaySnapshot,
1122    point: DisplayPoint,
1123    ignore_punctuation: bool,
1124    times: usize,
1125) -> DisplayPoint {
1126    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1127    let mut point = point.to_point(map);
1128
1129    if point.column < map.buffer_snapshot.line_len(point.row) {
1130        point.column += 1;
1131    }
1132    for _ in 0..times {
1133        let new_point = movement::find_preceding_boundary_point(
1134            &map.buffer_snapshot,
1135            point,
1136            FindRange::MultiLine,
1137            |left, right| {
1138                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1139                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1140                match (left_kind, right_kind) {
1141                    (CharKind::Punctuation, CharKind::Whitespace)
1142                    | (CharKind::Punctuation, CharKind::Word)
1143                    | (CharKind::Word, CharKind::Whitespace)
1144                    | (CharKind::Word, CharKind::Punctuation) => true,
1145                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1146                    _ => false,
1147                }
1148            },
1149        );
1150        if new_point == point {
1151            break;
1152        }
1153        point = new_point;
1154    }
1155    movement::saturating_left(map, point.to_display_point(map))
1156}
1157
1158fn next_subword_start(
1159    map: &DisplaySnapshot,
1160    mut point: DisplayPoint,
1161    ignore_punctuation: bool,
1162    times: usize,
1163) -> DisplayPoint {
1164    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1165    for _ in 0..times {
1166        let mut crossed_newline = false;
1167        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1168            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1169            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1170            let at_newline = right == '\n';
1171
1172            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1173            let is_subword_start =
1174                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1175
1176            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1177                || at_newline && crossed_newline
1178                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1179
1180            crossed_newline |= at_newline;
1181            found
1182        });
1183        if point == new_point {
1184            break;
1185        }
1186        point = new_point;
1187    }
1188    point
1189}
1190
1191pub(crate) fn next_subword_end(
1192    map: &DisplaySnapshot,
1193    mut point: DisplayPoint,
1194    ignore_punctuation: bool,
1195    times: usize,
1196    allow_cross_newline: bool,
1197) -> DisplayPoint {
1198    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1199    for _ in 0..times {
1200        let new_point = next_char(map, point, allow_cross_newline);
1201
1202        let mut crossed_newline = false;
1203        let mut need_backtrack = false;
1204        let new_point =
1205            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1206                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1207                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1208                let at_newline = right == '\n';
1209
1210                if !allow_cross_newline && at_newline {
1211                    return true;
1212                }
1213
1214                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1215                let is_subword_end =
1216                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1217
1218                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1219
1220                if found && (is_word_end || is_subword_end) {
1221                    need_backtrack = true;
1222                }
1223
1224                crossed_newline |= at_newline;
1225                found
1226            });
1227        let mut new_point = map.clip_point(new_point, Bias::Left);
1228        if need_backtrack {
1229            *new_point.column_mut() -= 1;
1230        }
1231        if point == new_point {
1232            break;
1233        }
1234        point = new_point;
1235    }
1236    point
1237}
1238
1239fn previous_subword_start(
1240    map: &DisplaySnapshot,
1241    mut point: DisplayPoint,
1242    ignore_punctuation: bool,
1243    times: usize,
1244) -> DisplayPoint {
1245    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1246    for _ in 0..times {
1247        let mut crossed_newline = false;
1248        // This works even though find_preceding_boundary is called for every character in the line containing
1249        // cursor because the newline is checked only once.
1250        let new_point = movement::find_preceding_boundary_display_point(
1251            map,
1252            point,
1253            FindRange::MultiLine,
1254            |left, right| {
1255                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1256                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1257                let at_newline = right == '\n';
1258
1259                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1260                let is_subword_start =
1261                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1262
1263                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1264                    || at_newline && crossed_newline
1265                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1266
1267                crossed_newline |= at_newline;
1268
1269                found
1270            },
1271        );
1272        if point == new_point {
1273            break;
1274        }
1275        point = new_point;
1276    }
1277    point
1278}
1279
1280fn previous_subword_end(
1281    map: &DisplaySnapshot,
1282    point: DisplayPoint,
1283    ignore_punctuation: bool,
1284    times: usize,
1285) -> DisplayPoint {
1286    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1287    let mut point = point.to_point(map);
1288
1289    if point.column < map.buffer_snapshot.line_len(point.row) {
1290        point.column += 1;
1291    }
1292    for _ in 0..times {
1293        let new_point = movement::find_preceding_boundary_point(
1294            &map.buffer_snapshot,
1295            point,
1296            FindRange::MultiLine,
1297            |left, right| {
1298                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1299                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1300
1301                let is_subword_end =
1302                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1303
1304                if is_subword_end {
1305                    return true;
1306                }
1307
1308                match (left_kind, right_kind) {
1309                    (CharKind::Word, CharKind::Whitespace)
1310                    | (CharKind::Word, CharKind::Punctuation) => true,
1311                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1312                    _ => false,
1313                }
1314            },
1315        );
1316        if new_point == point {
1317            break;
1318        }
1319        point = new_point;
1320    }
1321    movement::saturating_left(map, point.to_display_point(map))
1322}
1323
1324pub(crate) fn first_non_whitespace(
1325    map: &DisplaySnapshot,
1326    display_lines: bool,
1327    from: DisplayPoint,
1328) -> DisplayPoint {
1329    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1330    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1331    for (ch, offset) in map.buffer_chars_at(start_offset) {
1332        if ch == '\n' {
1333            return from;
1334        }
1335
1336        start_offset = offset;
1337
1338        if char_kind(&scope, ch) != CharKind::Whitespace {
1339            break;
1340        }
1341    }
1342
1343    start_offset.to_display_point(map)
1344}
1345
1346pub(crate) fn start_of_line(
1347    map: &DisplaySnapshot,
1348    display_lines: bool,
1349    point: DisplayPoint,
1350) -> DisplayPoint {
1351    if display_lines {
1352        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1353    } else {
1354        map.prev_line_boundary(point.to_point(map)).1
1355    }
1356}
1357
1358pub(crate) fn end_of_line(
1359    map: &DisplaySnapshot,
1360    display_lines: bool,
1361    mut point: DisplayPoint,
1362    times: usize,
1363) -> DisplayPoint {
1364    if times > 1 {
1365        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1366    }
1367    if display_lines {
1368        map.clip_point(
1369            DisplayPoint::new(point.row(), map.line_len(point.row())),
1370            Bias::Left,
1371        )
1372    } else {
1373        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1374    }
1375}
1376
1377fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1378    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1379    *new_point.column_mut() = point.column();
1380    map.clip_point(new_point, Bias::Left)
1381}
1382
1383fn end_of_document(
1384    map: &DisplaySnapshot,
1385    point: DisplayPoint,
1386    line: Option<usize>,
1387) -> DisplayPoint {
1388    let new_row = if let Some(line) = line {
1389        (line - 1) as u32
1390    } else {
1391        map.max_buffer_row()
1392    };
1393
1394    let new_point = Point::new(new_row, point.column());
1395    map.clip_point(new_point.to_display_point(map), Bias::Left)
1396}
1397
1398fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1399    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1400    let display_point = map.clip_at_line_end(display_point);
1401    let point = display_point.to_point(map);
1402    let offset = point.to_offset(&map.buffer_snapshot);
1403
1404    // Ensure the range is contained by the current line.
1405    let mut line_end = map.next_line_boundary(point).0;
1406    if line_end == point {
1407        line_end = map.max_point().to_point(map);
1408    }
1409
1410    let line_range = map.prev_line_boundary(point).0..line_end;
1411    let visible_line_range =
1412        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1413    let ranges = map
1414        .buffer_snapshot
1415        .bracket_ranges(visible_line_range.clone());
1416    if let Some(ranges) = ranges {
1417        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1418            ..line_range.end.to_offset(&map.buffer_snapshot);
1419        let mut closest_pair_destination = None;
1420        let mut closest_distance = usize::MAX;
1421
1422        for (open_range, close_range) in ranges {
1423            if open_range.start >= offset && line_range.contains(&open_range.start) {
1424                let distance = open_range.start - offset;
1425                if distance < closest_distance {
1426                    closest_pair_destination = Some(close_range.start);
1427                    closest_distance = distance;
1428                    continue;
1429                }
1430            }
1431
1432            if close_range.start >= offset && line_range.contains(&close_range.start) {
1433                let distance = close_range.start - offset;
1434                if distance < closest_distance {
1435                    closest_pair_destination = Some(open_range.start);
1436                    closest_distance = distance;
1437                    continue;
1438                }
1439            }
1440
1441            continue;
1442        }
1443
1444        closest_pair_destination
1445            .map(|destination| destination.to_display_point(map))
1446            .unwrap_or(display_point)
1447    } else {
1448        display_point
1449    }
1450}
1451
1452fn find_forward(
1453    map: &DisplaySnapshot,
1454    from: DisplayPoint,
1455    before: bool,
1456    target: char,
1457    times: usize,
1458    mode: FindRange,
1459    smartcase: bool,
1460) -> Option<DisplayPoint> {
1461    let mut to = from;
1462    let mut found = false;
1463
1464    for _ in 0..times {
1465        found = false;
1466        let new_to = find_boundary(map, to, mode, |_, right| {
1467            found = is_character_match(target, right, smartcase);
1468            found
1469        });
1470        if to == new_to {
1471            break;
1472        }
1473        to = new_to;
1474    }
1475
1476    if found {
1477        if before && to.column() > 0 {
1478            *to.column_mut() -= 1;
1479            Some(map.clip_point(to, Bias::Left))
1480        } else {
1481            Some(to)
1482        }
1483    } else {
1484        None
1485    }
1486}
1487
1488fn find_backward(
1489    map: &DisplaySnapshot,
1490    from: DisplayPoint,
1491    after: bool,
1492    target: char,
1493    times: usize,
1494    mode: FindRange,
1495    smartcase: bool,
1496) -> DisplayPoint {
1497    let mut to = from;
1498
1499    for _ in 0..times {
1500        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1501            is_character_match(target, right, smartcase)
1502        });
1503        if to == new_to {
1504            break;
1505        }
1506        to = new_to;
1507    }
1508
1509    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1510    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1511        if after {
1512            *to.column_mut() += 1;
1513            map.clip_point(to, Bias::Right)
1514        } else {
1515            to
1516        }
1517    } else {
1518        from
1519    }
1520}
1521
1522fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1523    if smartcase {
1524        if target.is_uppercase() {
1525            target == other
1526        } else {
1527            target == other.to_ascii_lowercase()
1528        }
1529    } else {
1530        target == other
1531    }
1532}
1533
1534fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1535    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1536    first_non_whitespace(map, false, correct_line)
1537}
1538
1539fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1540    let correct_line = start_of_relative_buffer_row(map, point, 0);
1541    right(map, correct_line, times.saturating_sub(1))
1542}
1543
1544pub(crate) fn next_line_end(
1545    map: &DisplaySnapshot,
1546    mut point: DisplayPoint,
1547    times: usize,
1548) -> DisplayPoint {
1549    if times > 1 {
1550        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1551    }
1552    end_of_line(map, false, point, 1)
1553}
1554
1555fn window_top(
1556    map: &DisplaySnapshot,
1557    point: DisplayPoint,
1558    text_layout_details: &TextLayoutDetails,
1559    mut times: usize,
1560) -> (DisplayPoint, SelectionGoal) {
1561    let first_visible_line = text_layout_details
1562        .scroll_anchor
1563        .anchor
1564        .to_display_point(map);
1565
1566    if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1567    {
1568        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1569    }
1570
1571    if let Some(visible_rows) = text_layout_details.visible_rows {
1572        let bottom_row = first_visible_line.row() + visible_rows as u32;
1573        let new_row = (first_visible_line.row() + (times as u32))
1574            .min(bottom_row)
1575            .min(map.max_point().row());
1576        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1577
1578        let new_point = DisplayPoint::new(new_row, new_col);
1579        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1580    } else {
1581        let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1582        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1583
1584        let new_point = DisplayPoint::new(new_row, new_col);
1585        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1586    }
1587}
1588
1589fn window_middle(
1590    map: &DisplaySnapshot,
1591    point: DisplayPoint,
1592    text_layout_details: &TextLayoutDetails,
1593) -> (DisplayPoint, SelectionGoal) {
1594    if let Some(visible_rows) = text_layout_details.visible_rows {
1595        let first_visible_line = text_layout_details
1596            .scroll_anchor
1597            .anchor
1598            .to_display_point(map);
1599
1600        let max_visible_rows =
1601            (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1602
1603        let new_row =
1604            (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1605        let new_col = point.column().min(map.line_len(new_row));
1606        let new_point = DisplayPoint::new(new_row, new_col);
1607        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1608    } else {
1609        (point, SelectionGoal::None)
1610    }
1611}
1612
1613fn window_bottom(
1614    map: &DisplaySnapshot,
1615    point: DisplayPoint,
1616    text_layout_details: &TextLayoutDetails,
1617    mut times: usize,
1618) -> (DisplayPoint, SelectionGoal) {
1619    if let Some(visible_rows) = text_layout_details.visible_rows {
1620        let first_visible_line = text_layout_details
1621            .scroll_anchor
1622            .anchor
1623            .to_display_point(map);
1624        let bottom_row = first_visible_line.row()
1625            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1626        if bottom_row < map.max_point().row()
1627            && text_layout_details.vertical_scroll_margin as usize > times
1628        {
1629            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1630        }
1631        let bottom_row_capped = bottom_row.min(map.max_point().row());
1632        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1633            first_visible_line.row()
1634        } else {
1635            bottom_row_capped.saturating_sub(times as u32)
1636        };
1637        let new_col = point.column().min(map.line_len(new_row));
1638        let new_point = DisplayPoint::new(new_row, new_col);
1639        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1640    } else {
1641        (point, SelectionGoal::None)
1642    }
1643}
1644
1645#[cfg(test)]
1646mod test {
1647
1648    use crate::test::NeovimBackedTestContext;
1649    use indoc::indoc;
1650
1651    #[gpui::test]
1652    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1653        let mut cx = NeovimBackedTestContext::new(cx).await;
1654
1655        let initial_state = indoc! {r"ˇabc
1656            def
1657
1658            paragraph
1659            the second
1660
1661
1662
1663            third and
1664            final"};
1665
1666        // goes down once
1667        cx.set_shared_state(initial_state).await;
1668        cx.simulate_shared_keystrokes(["}"]).await;
1669        cx.assert_shared_state(indoc! {r"abc
1670            def
1671            ˇ
1672            paragraph
1673            the second
1674
1675
1676
1677            third and
1678            final"})
1679            .await;
1680
1681        // goes up once
1682        cx.simulate_shared_keystrokes(["{"]).await;
1683        cx.assert_shared_state(initial_state).await;
1684
1685        // goes down twice
1686        cx.simulate_shared_keystrokes(["2", "}"]).await;
1687        cx.assert_shared_state(indoc! {r"abc
1688            def
1689
1690            paragraph
1691            the second
1692            ˇ
1693
1694
1695            third and
1696            final"})
1697            .await;
1698
1699        // goes down over multiple blanks
1700        cx.simulate_shared_keystrokes(["}"]).await;
1701        cx.assert_shared_state(indoc! {r"abc
1702                def
1703
1704                paragraph
1705                the second
1706
1707
1708
1709                third and
1710                finaˇl"})
1711            .await;
1712
1713        // goes up twice
1714        cx.simulate_shared_keystrokes(["2", "{"]).await;
1715        cx.assert_shared_state(indoc! {r"abc
1716                def
1717                ˇ
1718                paragraph
1719                the second
1720
1721
1722
1723                third and
1724                final"})
1725            .await
1726    }
1727
1728    #[gpui::test]
1729    async fn test_matching(cx: &mut gpui::TestAppContext) {
1730        let mut cx = NeovimBackedTestContext::new(cx).await;
1731
1732        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1733                do(something(with<Types>.and_arrays[0, 2]))
1734            }"})
1735            .await;
1736        cx.simulate_shared_keystrokes(["%"]).await;
1737        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1738                do(something(with<Types>.and_arrays[0, 2]))
1739            }"})
1740            .await;
1741
1742        // test it works on the last character of the line
1743        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1744            do(something(with<Types>.and_arrays[0, 2]))
1745            }"})
1746            .await;
1747        cx.simulate_shared_keystrokes(["%"]).await;
1748        cx.assert_shared_state(indoc! {r"func (a string) {
1749            do(something(with<Types>.and_arrays[0, 2]))
1750            ˇ}"})
1751            .await;
1752
1753        // test it works on immediate nesting
1754        cx.set_shared_state("ˇ{()}").await;
1755        cx.simulate_shared_keystrokes(["%"]).await;
1756        cx.assert_shared_state("{()ˇ}").await;
1757        cx.simulate_shared_keystrokes(["%"]).await;
1758        cx.assert_shared_state("ˇ{()}").await;
1759
1760        // test it works on immediate nesting inside braces
1761        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1762        cx.simulate_shared_keystrokes(["%"]).await;
1763        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1764
1765        // test it jumps to the next paren on a line
1766        cx.set_shared_state("func ˇboop() {\n}").await;
1767        cx.simulate_shared_keystrokes(["%"]).await;
1768        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1769    }
1770
1771    #[gpui::test]
1772    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1773        let mut cx = NeovimBackedTestContext::new(cx).await;
1774
1775        // f and F
1776        cx.set_shared_state("ˇone two three four").await;
1777        cx.simulate_shared_keystrokes(["f", "o"]).await;
1778        cx.assert_shared_state("one twˇo three four").await;
1779        cx.simulate_shared_keystrokes([","]).await;
1780        cx.assert_shared_state("ˇone two three four").await;
1781        cx.simulate_shared_keystrokes(["2", ";"]).await;
1782        cx.assert_shared_state("one two three fˇour").await;
1783        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1784        cx.assert_shared_state("one two threˇe four").await;
1785        cx.simulate_shared_keystrokes(["2", ";"]).await;
1786        cx.assert_shared_state("onˇe two three four").await;
1787        cx.simulate_shared_keystrokes([","]).await;
1788        cx.assert_shared_state("one two thrˇee four").await;
1789
1790        // t and T
1791        cx.set_shared_state("ˇone two three four").await;
1792        cx.simulate_shared_keystrokes(["t", "o"]).await;
1793        cx.assert_shared_state("one tˇwo three four").await;
1794        cx.simulate_shared_keystrokes([","]).await;
1795        cx.assert_shared_state("oˇne two three four").await;
1796        cx.simulate_shared_keystrokes(["2", ";"]).await;
1797        cx.assert_shared_state("one two three ˇfour").await;
1798        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1799        cx.assert_shared_state("one two threeˇ four").await;
1800        cx.simulate_shared_keystrokes(["3", ";"]).await;
1801        cx.assert_shared_state("oneˇ two three four").await;
1802        cx.simulate_shared_keystrokes([","]).await;
1803        cx.assert_shared_state("one two thˇree four").await;
1804    }
1805
1806    #[gpui::test]
1807    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1808        let mut cx = NeovimBackedTestContext::new(cx).await;
1809        let initial_state = indoc! {r"something(ˇfoo)"};
1810        cx.set_shared_state(initial_state).await;
1811        cx.simulate_shared_keystrokes(["}"]).await;
1812        cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1813    }
1814
1815    #[gpui::test]
1816    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1817        let mut cx = NeovimBackedTestContext::new(cx).await;
1818        cx.set_shared_state("ˇone\n  two\nthree").await;
1819        cx.simulate_shared_keystrokes(["enter"]).await;
1820        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1821    }
1822
1823    #[gpui::test]
1824    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1825        let mut cx = NeovimBackedTestContext::new(cx).await;
1826        let initial_state = indoc! {r"abc
1827          def
1828          paragraph
1829          the second
1830          third ˇand
1831          final"};
1832
1833        cx.set_shared_state(initial_state).await;
1834        cx.simulate_shared_keystrokes(["shift-h"]).await;
1835        cx.assert_shared_state(indoc! {r"abˇc
1836          def
1837          paragraph
1838          the second
1839          third and
1840          final"})
1841            .await;
1842
1843        // clip point
1844        cx.set_shared_state(indoc! {r"
1845          1 2 3
1846          4 5 6
1847          7 8 ˇ9
1848          "})
1849            .await;
1850        cx.simulate_shared_keystrokes(["shift-h"]).await;
1851        cx.assert_shared_state(indoc! {r"
1852          1 2 ˇ3
1853          4 5 6
1854          7 8 9
1855          "})
1856            .await;
1857
1858        cx.set_shared_state(indoc! {r"
1859          1 2 3
1860          4 5 6
1861          ˇ7 8 9
1862          "})
1863            .await;
1864        cx.simulate_shared_keystrokes(["shift-h"]).await;
1865        cx.assert_shared_state(indoc! {r"
1866          ˇ1 2 3
1867          4 5 6
1868          7 8 9
1869          "})
1870            .await;
1871
1872        cx.set_shared_state(indoc! {r"
1873          1 2 3
1874          4 5 ˇ6
1875          7 8 9"})
1876            .await;
1877        cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1878        cx.assert_shared_state(indoc! {r"
1879          1 2 3
1880          4 5 6
1881          7 8 ˇ9"})
1882            .await;
1883    }
1884
1885    #[gpui::test]
1886    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1887        let mut cx = NeovimBackedTestContext::new(cx).await;
1888        let initial_state = indoc! {r"abˇc
1889          def
1890          paragraph
1891          the second
1892          third and
1893          final"};
1894
1895        cx.set_shared_state(initial_state).await;
1896        cx.simulate_shared_keystrokes(["shift-m"]).await;
1897        cx.assert_shared_state(indoc! {r"abc
1898          def
1899          paˇragraph
1900          the second
1901          third and
1902          final"})
1903            .await;
1904
1905        cx.set_shared_state(indoc! {r"
1906          1 2 3
1907          4 5 6
1908          7 8 ˇ9
1909          "})
1910            .await;
1911        cx.simulate_shared_keystrokes(["shift-m"]).await;
1912        cx.assert_shared_state(indoc! {r"
1913          1 2 3
1914          4 5 ˇ6
1915          7 8 9
1916          "})
1917            .await;
1918        cx.set_shared_state(indoc! {r"
1919          1 2 3
1920          4 5 6
1921          ˇ7 8 9
1922          "})
1923            .await;
1924        cx.simulate_shared_keystrokes(["shift-m"]).await;
1925        cx.assert_shared_state(indoc! {r"
1926          1 2 3
1927          ˇ4 5 6
1928          7 8 9
1929          "})
1930            .await;
1931        cx.set_shared_state(indoc! {r"
1932          ˇ1 2 3
1933          4 5 6
1934          7 8 9
1935          "})
1936            .await;
1937        cx.simulate_shared_keystrokes(["shift-m"]).await;
1938        cx.assert_shared_state(indoc! {r"
1939          1 2 3
1940          ˇ4 5 6
1941          7 8 9
1942          "})
1943            .await;
1944        cx.set_shared_state(indoc! {r"
1945          1 2 3
1946          ˇ4 5 6
1947          7 8 9
1948          "})
1949            .await;
1950        cx.simulate_shared_keystrokes(["shift-m"]).await;
1951        cx.assert_shared_state(indoc! {r"
1952          1 2 3
1953          ˇ4 5 6
1954          7 8 9
1955          "})
1956            .await;
1957        cx.set_shared_state(indoc! {r"
1958          1 2 3
1959          4 5 ˇ6
1960          7 8 9
1961          "})
1962            .await;
1963        cx.simulate_shared_keystrokes(["shift-m"]).await;
1964        cx.assert_shared_state(indoc! {r"
1965          1 2 3
1966          4 5 ˇ6
1967          7 8 9
1968          "})
1969            .await;
1970    }
1971
1972    #[gpui::test]
1973    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1974        let mut cx = NeovimBackedTestContext::new(cx).await;
1975        let initial_state = indoc! {r"abc
1976          deˇf
1977          paragraph
1978          the second
1979          third and
1980          final"};
1981
1982        cx.set_shared_state(initial_state).await;
1983        cx.simulate_shared_keystrokes(["shift-l"]).await;
1984        cx.assert_shared_state(indoc! {r"abc
1985          def
1986          paragraph
1987          the second
1988          third and
1989          fiˇnal"})
1990            .await;
1991
1992        cx.set_shared_state(indoc! {r"
1993          1 2 3
1994          4 5 ˇ6
1995          7 8 9
1996          "})
1997            .await;
1998        cx.simulate_shared_keystrokes(["shift-l"]).await;
1999        cx.assert_shared_state(indoc! {r"
2000          1 2 3
2001          4 5 6
2002          7 8 9
2003          ˇ"})
2004            .await;
2005
2006        cx.set_shared_state(indoc! {r"
2007          1 2 3
2008          ˇ4 5 6
2009          7 8 9
2010          "})
2011            .await;
2012        cx.simulate_shared_keystrokes(["shift-l"]).await;
2013        cx.assert_shared_state(indoc! {r"
2014          1 2 3
2015          4 5 6
2016          7 8 9
2017          ˇ"})
2018            .await;
2019
2020        cx.set_shared_state(indoc! {r"
2021          1 2 ˇ3
2022          4 5 6
2023          7 8 9
2024          "})
2025            .await;
2026        cx.simulate_shared_keystrokes(["shift-l"]).await;
2027        cx.assert_shared_state(indoc! {r"
2028          1 2 3
2029          4 5 6
2030          7 8 9
2031          ˇ"})
2032            .await;
2033
2034        cx.set_shared_state(indoc! {r"
2035          ˇ1 2 3
2036          4 5 6
2037          7 8 9
2038          "})
2039            .await;
2040        cx.simulate_shared_keystrokes(["shift-l"]).await;
2041        cx.assert_shared_state(indoc! {r"
2042          1 2 3
2043          4 5 6
2044          7 8 9
2045          ˇ"})
2046            .await;
2047
2048        cx.set_shared_state(indoc! {r"
2049          1 2 3
2050          4 5 ˇ6
2051          7 8 9
2052          "})
2053            .await;
2054        cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
2055        cx.assert_shared_state(indoc! {r"
2056          1 2 ˇ3
2057          4 5 6
2058          7 8 9
2059          "})
2060            .await;
2061    }
2062
2063    #[gpui::test]
2064    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2065        let mut cx = NeovimBackedTestContext::new(cx).await;
2066        cx.set_shared_state(indoc! {r"
2067        456 5ˇ67 678
2068        "})
2069            .await;
2070        cx.simulate_shared_keystrokes(["g", "e"]).await;
2071        cx.assert_shared_state(indoc! {r"
2072        45ˇ6 567 678
2073        "})
2074            .await;
2075
2076        // Test times
2077        cx.set_shared_state(indoc! {r"
2078        123 234 345
2079        456 5ˇ67 678
2080        "})
2081            .await;
2082        cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2083        cx.assert_shared_state(indoc! {r"
2084        12ˇ3 234 345
2085        456 567 678
2086        "})
2087            .await;
2088
2089        // With punctuation
2090        cx.set_shared_state(indoc! {r"
2091        123 234 345
2092        4;5.6 5ˇ67 678
2093        789 890 901
2094        "})
2095            .await;
2096        cx.simulate_shared_keystrokes(["g", "e"]).await;
2097        cx.assert_shared_state(indoc! {r"
2098          123 234 345
2099          4;5.ˇ6 567 678
2100          789 890 901
2101        "})
2102            .await;
2103
2104        // With punctuation and count
2105        cx.set_shared_state(indoc! {r"
2106        123 234 345
2107        4;5.6 5ˇ67 678
2108        789 890 901
2109        "})
2110            .await;
2111        cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2112        cx.assert_shared_state(indoc! {r"
2113          123 234 345
2114          ˇ4;5.6 567 678
2115          789 890 901
2116        "})
2117            .await;
2118
2119        // newlines
2120        cx.set_shared_state(indoc! {r"
2121        123 234 345
2122
2123        78ˇ9 890 901
2124        "})
2125            .await;
2126        cx.simulate_shared_keystrokes(["g", "e"]).await;
2127        cx.assert_shared_state(indoc! {r"
2128          123 234 345
2129          ˇ
2130          789 890 901
2131        "})
2132            .await;
2133        cx.simulate_shared_keystrokes(["g", "e"]).await;
2134        cx.assert_shared_state(indoc! {r"
2135          123 234 34ˇ5
2136
2137          789 890 901
2138        "})
2139            .await;
2140
2141        // With punctuation
2142        cx.set_shared_state(indoc! {r"
2143        123 234 345
2144        4;5.ˇ6 567 678
2145        789 890 901
2146        "})
2147            .await;
2148        cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2149        cx.assert_shared_state(indoc! {r"
2150          123 234 34ˇ5
2151          4;5.6 567 678
2152          789 890 901
2153        "})
2154            .await;
2155    }
2156
2157    #[gpui::test]
2158    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2159        let mut cx = NeovimBackedTestContext::new(cx).await;
2160
2161        cx.set_shared_state(indoc! {"
2162            fn aˇ() {
2163              return
2164            }
2165        "})
2166            .await;
2167        cx.simulate_shared_keystrokes(["v", "$", "%"]).await;
2168        cx.assert_shared_state(indoc! {"
2169            fn a«() {
2170              return
2171            }ˇ»
2172        "})
2173            .await;
2174    }
2175}