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