motion.rs

   1use editor::{
   2    display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
   3    movement::{
   4        self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
   5    },
   6    scroll::Autoscroll,
   7    Anchor, Bias, DisplayPoint, RowExt, ToOffset,
   8};
   9use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
  10use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
  11use multi_buffer::MultiBufferRow;
  12use serde::Deserialize;
  13use std::ops::Range;
  14use workspace::Workspace;
  15
  16use crate::{
  17    normal::{mark, normal_motion},
  18    state::{Mode, Operator},
  19    surrounds::SurroundsType,
  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 > 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 = MultiBufferRow(selection.start.to_point(&map).row);
 864                    if selection.end.to_point(&map).row > start_row.0 {
 865                        selection.end =
 866                            Point::new(start_row.0, 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                let start_point = selection.start.to_point(&map);
 877                let mut end_point = selection.end.to_point(&map);
 878
 879                // DisplayPoint
 880
 881                if !inclusive
 882                    && self != &Motion::Backspace
 883                    && end_point.row > start_point.row
 884                    && end_point.column == 0
 885                {
 886                    inclusive = true;
 887                    end_point.row -= 1;
 888                    end_point.column = 0;
 889                    selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
 890                }
 891
 892                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 893                    *selection.end.column_mut() += 1;
 894                }
 895            }
 896            Some(selection.start..selection.end)
 897        } else {
 898            None
 899        }
 900    }
 901
 902    // Expands a selection using self for an operator
 903    pub fn expand_selection(
 904        &self,
 905        map: &DisplaySnapshot,
 906        selection: &mut Selection<DisplayPoint>,
 907        times: Option<usize>,
 908        expand_to_surrounding_newline: bool,
 909        text_layout_details: &TextLayoutDetails,
 910    ) -> bool {
 911        if let Some(range) = self.range(
 912            map,
 913            selection.clone(),
 914            times,
 915            expand_to_surrounding_newline,
 916            text_layout_details,
 917        ) {
 918            selection.start = range.start;
 919            selection.end = range.end;
 920            true
 921        } else {
 922            false
 923        }
 924    }
 925}
 926
 927fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 928    for _ in 0..times {
 929        point = movement::saturating_left(map, point);
 930        if point.column() == 0 {
 931            break;
 932        }
 933    }
 934    point
 935}
 936
 937pub(crate) fn backspace(
 938    map: &DisplaySnapshot,
 939    mut point: DisplayPoint,
 940    times: usize,
 941) -> DisplayPoint {
 942    for _ in 0..times {
 943        point = movement::left(map, point);
 944        if point.is_zero() {
 945            break;
 946        }
 947    }
 948    point
 949}
 950
 951fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 952    for _ in 0..times {
 953        point = wrapping_right(map, point);
 954        if point == map.max_point() {
 955            break;
 956        }
 957    }
 958    point
 959}
 960
 961fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 962    let max_column = map.line_len(point.row()).saturating_sub(1);
 963    if point.column() < max_column {
 964        *point.column_mut() += 1;
 965    } else if point.row() < map.max_point().row() {
 966        *point.row_mut() += 1;
 967        *point.column_mut() = 0;
 968    }
 969    point
 970}
 971
 972pub(crate) fn start_of_relative_buffer_row(
 973    map: &DisplaySnapshot,
 974    point: DisplayPoint,
 975    times: isize,
 976) -> DisplayPoint {
 977    let start = map.display_point_to_fold_point(point, Bias::Left);
 978    let target = start.row() as isize + times;
 979    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 980
 981    map.clip_point(
 982        map.fold_point_to_display_point(
 983            map.fold_snapshot
 984                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 985        ),
 986        Bias::Right,
 987    )
 988}
 989
 990fn up_down_buffer_rows(
 991    map: &DisplaySnapshot,
 992    point: DisplayPoint,
 993    mut goal: SelectionGoal,
 994    times: isize,
 995    text_layout_details: &TextLayoutDetails,
 996) -> (DisplayPoint, SelectionGoal) {
 997    let start = map.display_point_to_fold_point(point, Bias::Left);
 998    let begin_folded_line = map.fold_point_to_display_point(
 999        map.fold_snapshot
1000            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1001    );
1002    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1003
1004    let (goal_wrap, goal_x) = match goal {
1005        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1006        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1007        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1008        _ => {
1009            let x = map.x_for_display_point(point, text_layout_details);
1010            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1011            (select_nth_wrapped_row, x.0)
1012        }
1013    };
1014
1015    let target = start.row() as isize + times;
1016    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1017
1018    let mut begin_folded_line = map.fold_point_to_display_point(
1019        map.fold_snapshot
1020            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
1021    );
1022
1023    let mut i = 0;
1024    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1025        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1026        if map
1027            .display_point_to_fold_point(next_folded_line, Bias::Right)
1028            .row()
1029            == new_row
1030        {
1031            i += 1;
1032            begin_folded_line = next_folded_line;
1033        } else {
1034            break;
1035        }
1036    }
1037
1038    let new_col = if i == goal_wrap {
1039        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1040    } else {
1041        map.line_len(begin_folded_line.row())
1042    };
1043
1044    (
1045        map.clip_point(
1046            DisplayPoint::new(begin_folded_line.row(), new_col),
1047            Bias::Left,
1048        ),
1049        goal,
1050    )
1051}
1052
1053fn down_display(
1054    map: &DisplaySnapshot,
1055    mut point: DisplayPoint,
1056    mut goal: SelectionGoal,
1057    times: usize,
1058    text_layout_details: &TextLayoutDetails,
1059) -> (DisplayPoint, SelectionGoal) {
1060    for _ in 0..times {
1061        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1062    }
1063
1064    (point, goal)
1065}
1066
1067fn up_display(
1068    map: &DisplaySnapshot,
1069    mut point: DisplayPoint,
1070    mut goal: SelectionGoal,
1071    times: usize,
1072    text_layout_details: &TextLayoutDetails,
1073) -> (DisplayPoint, SelectionGoal) {
1074    for _ in 0..times {
1075        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
1076    }
1077
1078    (point, goal)
1079}
1080
1081pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1082    for _ in 0..times {
1083        let new_point = movement::saturating_right(map, point);
1084        if point == new_point {
1085            break;
1086        }
1087        point = new_point;
1088    }
1089    point
1090}
1091
1092pub(crate) fn next_char(
1093    map: &DisplaySnapshot,
1094    point: DisplayPoint,
1095    allow_cross_newline: bool,
1096) -> DisplayPoint {
1097    let mut new_point = point;
1098    let mut max_column = map.line_len(new_point.row());
1099    if !allow_cross_newline {
1100        max_column -= 1;
1101    }
1102    if new_point.column() < max_column {
1103        *new_point.column_mut() += 1;
1104    } else if new_point < map.max_point() && allow_cross_newline {
1105        *new_point.row_mut() += 1;
1106        *new_point.column_mut() = 0;
1107    }
1108    map.clip_ignoring_line_ends(new_point, Bias::Right)
1109}
1110
1111pub(crate) fn next_word_start(
1112    map: &DisplaySnapshot,
1113    mut point: DisplayPoint,
1114    ignore_punctuation: bool,
1115    times: usize,
1116) -> DisplayPoint {
1117    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1118    for _ in 0..times {
1119        let mut crossed_newline = false;
1120        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1121            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1122            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1123            let at_newline = right == '\n';
1124
1125            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1126                || at_newline && crossed_newline
1127                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1128
1129            crossed_newline |= at_newline;
1130            found
1131        });
1132        if point == new_point {
1133            break;
1134        }
1135        point = new_point;
1136    }
1137    point
1138}
1139
1140pub(crate) fn next_word_end(
1141    map: &DisplaySnapshot,
1142    mut point: DisplayPoint,
1143    ignore_punctuation: bool,
1144    times: usize,
1145    allow_cross_newline: bool,
1146) -> DisplayPoint {
1147    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1148    for _ in 0..times {
1149        let new_point = next_char(map, point, allow_cross_newline);
1150        let mut need_next_char = false;
1151        let new_point = movement::find_boundary_exclusive(
1152            map,
1153            new_point,
1154            FindRange::MultiLine,
1155            |left, right| {
1156                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1157                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1158                let at_newline = right == '\n';
1159
1160                if !allow_cross_newline && at_newline {
1161                    need_next_char = true;
1162                    return true;
1163                }
1164
1165                left_kind != right_kind && left_kind != CharKind::Whitespace
1166            },
1167        );
1168        let new_point = if need_next_char {
1169            next_char(map, new_point, true)
1170        } else {
1171            new_point
1172        };
1173        let new_point = map.clip_point(new_point, Bias::Left);
1174        if point == new_point {
1175            break;
1176        }
1177        point = new_point;
1178    }
1179    point
1180}
1181
1182fn previous_word_start(
1183    map: &DisplaySnapshot,
1184    mut point: DisplayPoint,
1185    ignore_punctuation: bool,
1186    times: usize,
1187) -> DisplayPoint {
1188    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1189    for _ in 0..times {
1190        // This works even though find_preceding_boundary is called for every character in the line containing
1191        // cursor because the newline is checked only once.
1192        let new_point = movement::find_preceding_boundary_display_point(
1193            map,
1194            point,
1195            FindRange::MultiLine,
1196            |left, right| {
1197                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1198                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1199
1200                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1201            },
1202        );
1203        if point == new_point {
1204            break;
1205        }
1206        point = new_point;
1207    }
1208    point
1209}
1210
1211fn previous_word_end(
1212    map: &DisplaySnapshot,
1213    point: DisplayPoint,
1214    ignore_punctuation: bool,
1215    times: usize,
1216) -> DisplayPoint {
1217    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1218    let mut point = point.to_point(map);
1219
1220    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1221        point.column += 1;
1222    }
1223    for _ in 0..times {
1224        let new_point = movement::find_preceding_boundary_point(
1225            &map.buffer_snapshot,
1226            point,
1227            FindRange::MultiLine,
1228            |left, right| {
1229                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1230                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1231                match (left_kind, right_kind) {
1232                    (CharKind::Punctuation, CharKind::Whitespace)
1233                    | (CharKind::Punctuation, CharKind::Word)
1234                    | (CharKind::Word, CharKind::Whitespace)
1235                    | (CharKind::Word, CharKind::Punctuation) => true,
1236                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1237                    _ => false,
1238                }
1239            },
1240        );
1241        if new_point == point {
1242            break;
1243        }
1244        point = new_point;
1245    }
1246    movement::saturating_left(map, point.to_display_point(map))
1247}
1248
1249fn next_subword_start(
1250    map: &DisplaySnapshot,
1251    mut point: DisplayPoint,
1252    ignore_punctuation: bool,
1253    times: usize,
1254) -> DisplayPoint {
1255    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1256    for _ in 0..times {
1257        let mut crossed_newline = false;
1258        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1259            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1260            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1261            let at_newline = right == '\n';
1262
1263            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1264            let is_subword_start =
1265                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1266
1267            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1268                || at_newline && crossed_newline
1269                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1270
1271            crossed_newline |= at_newline;
1272            found
1273        });
1274        if point == new_point {
1275            break;
1276        }
1277        point = new_point;
1278    }
1279    point
1280}
1281
1282pub(crate) fn next_subword_end(
1283    map: &DisplaySnapshot,
1284    mut point: DisplayPoint,
1285    ignore_punctuation: bool,
1286    times: usize,
1287    allow_cross_newline: bool,
1288) -> DisplayPoint {
1289    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1290    for _ in 0..times {
1291        let new_point = next_char(map, point, allow_cross_newline);
1292
1293        let mut crossed_newline = false;
1294        let mut need_backtrack = false;
1295        let new_point =
1296            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1297                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1298                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1299                let at_newline = right == '\n';
1300
1301                if !allow_cross_newline && at_newline {
1302                    return true;
1303                }
1304
1305                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1306                let is_subword_end =
1307                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1308
1309                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1310
1311                if found && (is_word_end || is_subword_end) {
1312                    need_backtrack = true;
1313                }
1314
1315                crossed_newline |= at_newline;
1316                found
1317            });
1318        let mut new_point = map.clip_point(new_point, Bias::Left);
1319        if need_backtrack {
1320            *new_point.column_mut() -= 1;
1321        }
1322        if point == new_point {
1323            break;
1324        }
1325        point = new_point;
1326    }
1327    point
1328}
1329
1330fn previous_subword_start(
1331    map: &DisplaySnapshot,
1332    mut point: DisplayPoint,
1333    ignore_punctuation: bool,
1334    times: usize,
1335) -> DisplayPoint {
1336    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1337    for _ in 0..times {
1338        let mut crossed_newline = false;
1339        // This works even though find_preceding_boundary is called for every character in the line containing
1340        // cursor because the newline is checked only once.
1341        let new_point = movement::find_preceding_boundary_display_point(
1342            map,
1343            point,
1344            FindRange::MultiLine,
1345            |left, right| {
1346                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1347                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1348                let at_newline = right == '\n';
1349
1350                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1351                let is_subword_start =
1352                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1353
1354                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1355                    || at_newline && crossed_newline
1356                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1357
1358                crossed_newline |= at_newline;
1359
1360                found
1361            },
1362        );
1363        if point == new_point {
1364            break;
1365        }
1366        point = new_point;
1367    }
1368    point
1369}
1370
1371fn previous_subword_end(
1372    map: &DisplaySnapshot,
1373    point: DisplayPoint,
1374    ignore_punctuation: bool,
1375    times: usize,
1376) -> DisplayPoint {
1377    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1378    let mut point = point.to_point(map);
1379
1380    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1381        point.column += 1;
1382    }
1383    for _ in 0..times {
1384        let new_point = movement::find_preceding_boundary_point(
1385            &map.buffer_snapshot,
1386            point,
1387            FindRange::MultiLine,
1388            |left, right| {
1389                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1390                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1391
1392                let is_subword_end =
1393                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1394
1395                if is_subword_end {
1396                    return true;
1397                }
1398
1399                match (left_kind, right_kind) {
1400                    (CharKind::Word, CharKind::Whitespace)
1401                    | (CharKind::Word, CharKind::Punctuation) => true,
1402                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1403                    _ => false,
1404                }
1405            },
1406        );
1407        if new_point == point {
1408            break;
1409        }
1410        point = new_point;
1411    }
1412    movement::saturating_left(map, point.to_display_point(map))
1413}
1414
1415pub(crate) fn first_non_whitespace(
1416    map: &DisplaySnapshot,
1417    display_lines: bool,
1418    from: DisplayPoint,
1419) -> DisplayPoint {
1420    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1421    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1422    for (ch, offset) in map.buffer_chars_at(start_offset) {
1423        if ch == '\n' {
1424            return from;
1425        }
1426
1427        start_offset = offset;
1428
1429        if char_kind(&scope, ch) != CharKind::Whitespace {
1430            break;
1431        }
1432    }
1433
1434    start_offset.to_display_point(map)
1435}
1436
1437pub(crate) fn last_non_whitespace(
1438    map: &DisplaySnapshot,
1439    from: DisplayPoint,
1440    count: usize,
1441) -> DisplayPoint {
1442    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1443    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1444
1445    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1446    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1447        if char_kind(&scope, ch) != CharKind::Whitespace {
1448            return end_of_line.to_display_point(map);
1449        }
1450    }
1451
1452    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1453        if ch == '\n' {
1454            break;
1455        }
1456        end_of_line = offset;
1457        if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
1458            break;
1459        }
1460    }
1461
1462    end_of_line.to_display_point(map)
1463}
1464
1465pub(crate) fn start_of_line(
1466    map: &DisplaySnapshot,
1467    display_lines: bool,
1468    point: DisplayPoint,
1469) -> DisplayPoint {
1470    if display_lines {
1471        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1472    } else {
1473        map.prev_line_boundary(point.to_point(map)).1
1474    }
1475}
1476
1477pub(crate) fn end_of_line(
1478    map: &DisplaySnapshot,
1479    display_lines: bool,
1480    mut point: DisplayPoint,
1481    times: usize,
1482) -> DisplayPoint {
1483    if times > 1 {
1484        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1485    }
1486    if display_lines {
1487        map.clip_point(
1488            DisplayPoint::new(point.row(), map.line_len(point.row())),
1489            Bias::Left,
1490        )
1491    } else {
1492        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1493    }
1494}
1495
1496fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1497    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1498    *new_point.column_mut() = point.column();
1499    map.clip_point(new_point, Bias::Left)
1500}
1501
1502fn end_of_document(
1503    map: &DisplaySnapshot,
1504    point: DisplayPoint,
1505    line: Option<usize>,
1506) -> DisplayPoint {
1507    let new_row = if let Some(line) = line {
1508        (line - 1) as u32
1509    } else {
1510        map.max_buffer_row().0
1511    };
1512
1513    let new_point = Point::new(new_row, point.column());
1514    map.clip_point(new_point.to_display_point(map), Bias::Left)
1515}
1516
1517fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1518    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1519    let display_point = map.clip_at_line_end(display_point);
1520    let point = display_point.to_point(map);
1521    let offset = point.to_offset(&map.buffer_snapshot);
1522
1523    // Ensure the range is contained by the current line.
1524    let mut line_end = map.next_line_boundary(point).0;
1525    if line_end == point {
1526        line_end = map.max_point().to_point(map);
1527    }
1528
1529    let line_range = map.prev_line_boundary(point).0..line_end;
1530    let visible_line_range =
1531        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1532    let ranges = map
1533        .buffer_snapshot
1534        .bracket_ranges(visible_line_range.clone());
1535    if let Some(ranges) = ranges {
1536        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1537            ..line_range.end.to_offset(&map.buffer_snapshot);
1538        let mut closest_pair_destination = None;
1539        let mut closest_distance = usize::MAX;
1540
1541        for (open_range, close_range) in ranges {
1542            if open_range.start >= offset && line_range.contains(&open_range.start) {
1543                let distance = open_range.start - offset;
1544                if distance < closest_distance {
1545                    closest_pair_destination = Some(close_range.start);
1546                    closest_distance = distance;
1547                    continue;
1548                }
1549            }
1550
1551            if close_range.start >= offset && line_range.contains(&close_range.start) {
1552                let distance = close_range.start - offset;
1553                if distance < closest_distance {
1554                    closest_pair_destination = Some(open_range.start);
1555                    closest_distance = distance;
1556                    continue;
1557                }
1558            }
1559
1560            continue;
1561        }
1562
1563        closest_pair_destination
1564            .map(|destination| destination.to_display_point(map))
1565            .unwrap_or(display_point)
1566    } else {
1567        display_point
1568    }
1569}
1570
1571fn find_forward(
1572    map: &DisplaySnapshot,
1573    from: DisplayPoint,
1574    before: bool,
1575    target: char,
1576    times: usize,
1577    mode: FindRange,
1578    smartcase: bool,
1579) -> Option<DisplayPoint> {
1580    let mut to = from;
1581    let mut found = false;
1582
1583    for _ in 0..times {
1584        found = false;
1585        let new_to = find_boundary(map, to, mode, |_, right| {
1586            found = is_character_match(target, right, smartcase);
1587            found
1588        });
1589        if to == new_to {
1590            break;
1591        }
1592        to = new_to;
1593    }
1594
1595    if found {
1596        if before && to.column() > 0 {
1597            *to.column_mut() -= 1;
1598            Some(map.clip_point(to, Bias::Left))
1599        } else {
1600            Some(to)
1601        }
1602    } else {
1603        None
1604    }
1605}
1606
1607fn find_backward(
1608    map: &DisplaySnapshot,
1609    from: DisplayPoint,
1610    after: bool,
1611    target: char,
1612    times: usize,
1613    mode: FindRange,
1614    smartcase: bool,
1615) -> DisplayPoint {
1616    let mut to = from;
1617
1618    for _ in 0..times {
1619        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1620            is_character_match(target, right, smartcase)
1621        });
1622        if to == new_to {
1623            break;
1624        }
1625        to = new_to;
1626    }
1627
1628    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1629    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1630        if after {
1631            *to.column_mut() += 1;
1632            map.clip_point(to, Bias::Right)
1633        } else {
1634            to
1635        }
1636    } else {
1637        from
1638    }
1639}
1640
1641fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1642    if smartcase {
1643        if target.is_uppercase() {
1644            target == other
1645        } else {
1646            target == other.to_ascii_lowercase()
1647        }
1648    } else {
1649        target == other
1650    }
1651}
1652
1653fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1654    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1655    first_non_whitespace(map, false, correct_line)
1656}
1657
1658fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1659    let correct_line = start_of_relative_buffer_row(map, point, 0);
1660    right(map, correct_line, times.saturating_sub(1))
1661}
1662
1663pub(crate) fn next_line_end(
1664    map: &DisplaySnapshot,
1665    mut point: DisplayPoint,
1666    times: usize,
1667) -> DisplayPoint {
1668    if times > 1 {
1669        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1670    }
1671    end_of_line(map, false, point, 1)
1672}
1673
1674fn window_top(
1675    map: &DisplaySnapshot,
1676    point: DisplayPoint,
1677    text_layout_details: &TextLayoutDetails,
1678    mut times: usize,
1679) -> (DisplayPoint, SelectionGoal) {
1680    let first_visible_line = text_layout_details
1681        .scroll_anchor
1682        .anchor
1683        .to_display_point(map);
1684
1685    if first_visible_line.row() != DisplayRow(0)
1686        && text_layout_details.vertical_scroll_margin as usize > times
1687    {
1688        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1689    }
1690
1691    if let Some(visible_rows) = text_layout_details.visible_rows {
1692        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1693        let new_row = (first_visible_line.row().0 + (times as u32))
1694            .min(bottom_row)
1695            .min(map.max_point().row().0);
1696        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1697
1698        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1699        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1700    } else {
1701        let new_row =
1702            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1703        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1704
1705        let new_point = DisplayPoint::new(new_row, new_col);
1706        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1707    }
1708}
1709
1710fn window_middle(
1711    map: &DisplaySnapshot,
1712    point: DisplayPoint,
1713    text_layout_details: &TextLayoutDetails,
1714) -> (DisplayPoint, SelectionGoal) {
1715    if let Some(visible_rows) = text_layout_details.visible_rows {
1716        let first_visible_line = text_layout_details
1717            .scroll_anchor
1718            .anchor
1719            .to_display_point(map);
1720
1721        let max_visible_rows =
1722            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1723
1724        let new_row =
1725            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1726        let new_row = DisplayRow(new_row);
1727        let new_col = point.column().min(map.line_len(new_row));
1728        let new_point = DisplayPoint::new(new_row, new_col);
1729        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1730    } else {
1731        (point, SelectionGoal::None)
1732    }
1733}
1734
1735fn window_bottom(
1736    map: &DisplaySnapshot,
1737    point: DisplayPoint,
1738    text_layout_details: &TextLayoutDetails,
1739    mut times: usize,
1740) -> (DisplayPoint, SelectionGoal) {
1741    if let Some(visible_rows) = text_layout_details.visible_rows {
1742        let first_visible_line = text_layout_details
1743            .scroll_anchor
1744            .anchor
1745            .to_display_point(map);
1746        let bottom_row = first_visible_line.row().0
1747            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1748        if bottom_row < map.max_point().row().0
1749            && text_layout_details.vertical_scroll_margin as usize > times
1750        {
1751            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1752        }
1753        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1754        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1755        {
1756            first_visible_line.row()
1757        } else {
1758            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1759        };
1760        let new_col = point.column().min(map.line_len(new_row));
1761        let new_point = DisplayPoint::new(new_row, new_col);
1762        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1763    } else {
1764        (point, SelectionGoal::None)
1765    }
1766}
1767
1768pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind {
1769    if treat_punctuation_as_word && kind == CharKind::Punctuation {
1770        CharKind::Word
1771    } else {
1772        kind
1773    }
1774}
1775
1776#[cfg(test)]
1777mod test {
1778
1779    use crate::test::NeovimBackedTestContext;
1780    use indoc::indoc;
1781
1782    #[gpui::test]
1783    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1784        let mut cx = NeovimBackedTestContext::new(cx).await;
1785
1786        let initial_state = indoc! {r"ˇabc
1787            def
1788
1789            paragraph
1790            the second
1791
1792
1793
1794            third and
1795            final"};
1796
1797        // goes down once
1798        cx.set_shared_state(initial_state).await;
1799        cx.simulate_shared_keystrokes("}").await;
1800        cx.shared_state().await.assert_eq(indoc! {r"abc
1801            def
1802            ˇ
1803            paragraph
1804            the second
1805
1806
1807
1808            third and
1809            final"});
1810
1811        // goes up once
1812        cx.simulate_shared_keystrokes("{").await;
1813        cx.shared_state().await.assert_eq(initial_state);
1814
1815        // goes down twice
1816        cx.simulate_shared_keystrokes("2 }").await;
1817        cx.shared_state().await.assert_eq(indoc! {r"abc
1818            def
1819
1820            paragraph
1821            the second
1822            ˇ
1823
1824
1825            third and
1826            final"});
1827
1828        // goes down over multiple blanks
1829        cx.simulate_shared_keystrokes("}").await;
1830        cx.shared_state().await.assert_eq(indoc! {r"abc
1831                def
1832
1833                paragraph
1834                the second
1835
1836
1837
1838                third and
1839                finaˇl"});
1840
1841        // goes up twice
1842        cx.simulate_shared_keystrokes("2 {").await;
1843        cx.shared_state().await.assert_eq(indoc! {r"abc
1844                def
1845                ˇ
1846                paragraph
1847                the second
1848
1849
1850
1851                third and
1852                final"});
1853    }
1854
1855    #[gpui::test]
1856    async fn test_matching(cx: &mut gpui::TestAppContext) {
1857        let mut cx = NeovimBackedTestContext::new(cx).await;
1858
1859        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1860                do(something(with<Types>.and_arrays[0, 2]))
1861            }"})
1862            .await;
1863        cx.simulate_shared_keystrokes("%").await;
1864        cx.shared_state()
1865            .await
1866            .assert_eq(indoc! {r"func (a stringˇ) {
1867                do(something(with<Types>.and_arrays[0, 2]))
1868            }"});
1869
1870        // test it works on the last character of the line
1871        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1872            do(something(with<Types>.and_arrays[0, 2]))
1873            }"})
1874            .await;
1875        cx.simulate_shared_keystrokes("%").await;
1876        cx.shared_state()
1877            .await
1878            .assert_eq(indoc! {r"func (a string) {
1879            do(something(with<Types>.and_arrays[0, 2]))
1880            ˇ}"});
1881
1882        // test it works on immediate nesting
1883        cx.set_shared_state("ˇ{()}").await;
1884        cx.simulate_shared_keystrokes("%").await;
1885        cx.shared_state().await.assert_eq("{()ˇ}");
1886        cx.simulate_shared_keystrokes("%").await;
1887        cx.shared_state().await.assert_eq("ˇ{()}");
1888
1889        // test it works on immediate nesting inside braces
1890        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1891        cx.simulate_shared_keystrokes("%").await;
1892        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
1893
1894        // test it jumps to the next paren on a line
1895        cx.set_shared_state("func ˇboop() {\n}").await;
1896        cx.simulate_shared_keystrokes("%").await;
1897        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
1898    }
1899
1900    #[gpui::test]
1901    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1902        let mut cx = NeovimBackedTestContext::new(cx).await;
1903
1904        // f and F
1905        cx.set_shared_state("ˇone two three four").await;
1906        cx.simulate_shared_keystrokes("f o").await;
1907        cx.shared_state().await.assert_eq("one twˇo three four");
1908        cx.simulate_shared_keystrokes(",").await;
1909        cx.shared_state().await.assert_eq("ˇone two three four");
1910        cx.simulate_shared_keystrokes("2 ;").await;
1911        cx.shared_state().await.assert_eq("one two three fˇour");
1912        cx.simulate_shared_keystrokes("shift-f e").await;
1913        cx.shared_state().await.assert_eq("one two threˇe four");
1914        cx.simulate_shared_keystrokes("2 ;").await;
1915        cx.shared_state().await.assert_eq("onˇe two three four");
1916        cx.simulate_shared_keystrokes(",").await;
1917        cx.shared_state().await.assert_eq("one two thrˇee four");
1918
1919        // t and T
1920        cx.set_shared_state("ˇone two three four").await;
1921        cx.simulate_shared_keystrokes("t o").await;
1922        cx.shared_state().await.assert_eq("one tˇwo three four");
1923        cx.simulate_shared_keystrokes(",").await;
1924        cx.shared_state().await.assert_eq("oˇne two three four");
1925        cx.simulate_shared_keystrokes("2 ;").await;
1926        cx.shared_state().await.assert_eq("one two three ˇfour");
1927        cx.simulate_shared_keystrokes("shift-t e").await;
1928        cx.shared_state().await.assert_eq("one two threeˇ four");
1929        cx.simulate_shared_keystrokes("3 ;").await;
1930        cx.shared_state().await.assert_eq("oneˇ two three four");
1931        cx.simulate_shared_keystrokes(",").await;
1932        cx.shared_state().await.assert_eq("one two thˇree four");
1933    }
1934
1935    #[gpui::test]
1936    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1937        let mut cx = NeovimBackedTestContext::new(cx).await;
1938        let initial_state = indoc! {r"something(ˇfoo)"};
1939        cx.set_shared_state(initial_state).await;
1940        cx.simulate_shared_keystrokes("}").await;
1941        cx.shared_state().await.assert_eq("something(fooˇ)");
1942    }
1943
1944    #[gpui::test]
1945    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1946        let mut cx = NeovimBackedTestContext::new(cx).await;
1947        cx.set_shared_state("ˇone\n  two\nthree").await;
1948        cx.simulate_shared_keystrokes("enter").await;
1949        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
1950    }
1951
1952    #[gpui::test]
1953    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1954        let mut cx = NeovimBackedTestContext::new(cx).await;
1955        cx.set_shared_state("ˇ one\n two \nthree").await;
1956        cx.simulate_shared_keystrokes("g _").await;
1957        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
1958
1959        cx.set_shared_state("ˇ one \n two \nthree").await;
1960        cx.simulate_shared_keystrokes("g _").await;
1961        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
1962        cx.simulate_shared_keystrokes("2 g _").await;
1963        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
1964    }
1965
1966    #[gpui::test]
1967    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1968        let mut cx = NeovimBackedTestContext::new(cx).await;
1969        let initial_state = indoc! {r"abc
1970          def
1971          paragraph
1972          the second
1973          third ˇand
1974          final"};
1975
1976        cx.set_shared_state(initial_state).await;
1977        cx.simulate_shared_keystrokes("shift-h").await;
1978        cx.shared_state().await.assert_eq(indoc! {r"abˇc
1979          def
1980          paragraph
1981          the second
1982          third and
1983          final"});
1984
1985        // clip point
1986        cx.set_shared_state(indoc! {r"
1987          1 2 3
1988          4 5 6
1989          7 8 ˇ9
1990          "})
1991            .await;
1992        cx.simulate_shared_keystrokes("shift-h").await;
1993        cx.shared_state().await.assert_eq(indoc! {"
1994          1 2 ˇ3
1995          4 5 6
1996          7 8 9
1997          "});
1998
1999        cx.set_shared_state(indoc! {r"
2000          1 2 3
2001          4 5 6
2002          ˇ7 8 9
2003          "})
2004            .await;
2005        cx.simulate_shared_keystrokes("shift-h").await;
2006        cx.shared_state().await.assert_eq(indoc! {"
2007          ˇ1 2 3
2008          4 5 6
2009          7 8 9
2010          "});
2011
2012        cx.set_shared_state(indoc! {r"
2013          1 2 3
2014          4 5 ˇ6
2015          7 8 9"})
2016            .await;
2017        cx.simulate_shared_keystrokes("9 shift-h").await;
2018        cx.shared_state().await.assert_eq(indoc! {"
2019          1 2 3
2020          4 5 6
2021          7 8 ˇ9"});
2022    }
2023
2024    #[gpui::test]
2025    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2026        let mut cx = NeovimBackedTestContext::new(cx).await;
2027        let initial_state = indoc! {r"abˇc
2028          def
2029          paragraph
2030          the second
2031          third and
2032          final"};
2033
2034        cx.set_shared_state(initial_state).await;
2035        cx.simulate_shared_keystrokes("shift-m").await;
2036        cx.shared_state().await.assert_eq(indoc! {r"abc
2037          def
2038          paˇragraph
2039          the second
2040          third and
2041          final"});
2042
2043        cx.set_shared_state(indoc! {r"
2044          1 2 3
2045          4 5 6
2046          7 8 ˇ9
2047          "})
2048            .await;
2049        cx.simulate_shared_keystrokes("shift-m").await;
2050        cx.shared_state().await.assert_eq(indoc! {"
2051          1 2 3
2052          4 5 ˇ6
2053          7 8 9
2054          "});
2055        cx.set_shared_state(indoc! {r"
2056          1 2 3
2057          4 5 6
2058          ˇ7 8 9
2059          "})
2060            .await;
2061        cx.simulate_shared_keystrokes("shift-m").await;
2062        cx.shared_state().await.assert_eq(indoc! {"
2063          1 2 3
2064          ˇ4 5 6
2065          7 8 9
2066          "});
2067        cx.set_shared_state(indoc! {r"
2068          ˇ1 2 3
2069          4 5 6
2070          7 8 9
2071          "})
2072            .await;
2073        cx.simulate_shared_keystrokes("shift-m").await;
2074        cx.shared_state().await.assert_eq(indoc! {"
2075          1 2 3
2076          ˇ4 5 6
2077          7 8 9
2078          "});
2079        cx.set_shared_state(indoc! {r"
2080          1 2 3
2081          ˇ4 5 6
2082          7 8 9
2083          "})
2084            .await;
2085        cx.simulate_shared_keystrokes("shift-m").await;
2086        cx.shared_state().await.assert_eq(indoc! {"
2087          1 2 3
2088          ˇ4 5 6
2089          7 8 9
2090          "});
2091        cx.set_shared_state(indoc! {r"
2092          1 2 3
2093          4 5 ˇ6
2094          7 8 9
2095          "})
2096            .await;
2097        cx.simulate_shared_keystrokes("shift-m").await;
2098        cx.shared_state().await.assert_eq(indoc! {"
2099          1 2 3
2100          4 5 ˇ6
2101          7 8 9
2102          "});
2103    }
2104
2105    #[gpui::test]
2106    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2107        let mut cx = NeovimBackedTestContext::new(cx).await;
2108        let initial_state = indoc! {r"abc
2109          deˇf
2110          paragraph
2111          the second
2112          third and
2113          final"};
2114
2115        cx.set_shared_state(initial_state).await;
2116        cx.simulate_shared_keystrokes("shift-l").await;
2117        cx.shared_state().await.assert_eq(indoc! {r"abc
2118          def
2119          paragraph
2120          the second
2121          third and
2122          fiˇnal"});
2123
2124        cx.set_shared_state(indoc! {r"
2125          1 2 3
2126          4 5 ˇ6
2127          7 8 9
2128          "})
2129            .await;
2130        cx.simulate_shared_keystrokes("shift-l").await;
2131        cx.shared_state().await.assert_eq(indoc! {"
2132          1 2 3
2133          4 5 6
2134          7 8 9
2135          ˇ"});
2136
2137        cx.set_shared_state(indoc! {r"
2138          1 2 3
2139          ˇ4 5 6
2140          7 8 9
2141          "})
2142            .await;
2143        cx.simulate_shared_keystrokes("shift-l").await;
2144        cx.shared_state().await.assert_eq(indoc! {"
2145          1 2 3
2146          4 5 6
2147          7 8 9
2148          ˇ"});
2149
2150        cx.set_shared_state(indoc! {r"
2151          1 2 ˇ3
2152          4 5 6
2153          7 8 9
2154          "})
2155            .await;
2156        cx.simulate_shared_keystrokes("shift-l").await;
2157        cx.shared_state().await.assert_eq(indoc! {"
2158          1 2 3
2159          4 5 6
2160          7 8 9
2161          ˇ"});
2162
2163        cx.set_shared_state(indoc! {r"
2164          ˇ1 2 3
2165          4 5 6
2166          7 8 9
2167          "})
2168            .await;
2169        cx.simulate_shared_keystrokes("shift-l").await;
2170        cx.shared_state().await.assert_eq(indoc! {"
2171          1 2 3
2172          4 5 6
2173          7 8 9
2174          ˇ"});
2175
2176        cx.set_shared_state(indoc! {r"
2177          1 2 3
2178          4 5 ˇ6
2179          7 8 9
2180          "})
2181            .await;
2182        cx.simulate_shared_keystrokes("9 shift-l").await;
2183        cx.shared_state().await.assert_eq(indoc! {"
2184          1 2 ˇ3
2185          4 5 6
2186          7 8 9
2187          "});
2188    }
2189
2190    #[gpui::test]
2191    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2192        let mut cx = NeovimBackedTestContext::new(cx).await;
2193        cx.set_shared_state(indoc! {r"
2194        456 5ˇ67 678
2195        "})
2196            .await;
2197        cx.simulate_shared_keystrokes("g e").await;
2198        cx.shared_state().await.assert_eq(indoc! {"
2199        45ˇ6 567 678
2200        "});
2201
2202        // Test times
2203        cx.set_shared_state(indoc! {r"
2204        123 234 345
2205        456 5ˇ67 678
2206        "})
2207            .await;
2208        cx.simulate_shared_keystrokes("4 g e").await;
2209        cx.shared_state().await.assert_eq(indoc! {"
2210        12ˇ3 234 345
2211        456 567 678
2212        "});
2213
2214        // With punctuation
2215        cx.set_shared_state(indoc! {r"
2216        123 234 345
2217        4;5.6 5ˇ67 678
2218        789 890 901
2219        "})
2220            .await;
2221        cx.simulate_shared_keystrokes("g e").await;
2222        cx.shared_state().await.assert_eq(indoc! {"
2223          123 234 345
2224          4;5.ˇ6 567 678
2225          789 890 901
2226        "});
2227
2228        // With punctuation and count
2229        cx.set_shared_state(indoc! {r"
2230        123 234 345
2231        4;5.6 5ˇ67 678
2232        789 890 901
2233        "})
2234            .await;
2235        cx.simulate_shared_keystrokes("5 g e").await;
2236        cx.shared_state().await.assert_eq(indoc! {"
2237          123 234 345
2238          ˇ4;5.6 567 678
2239          789 890 901
2240        "});
2241
2242        // newlines
2243        cx.set_shared_state(indoc! {r"
2244        123 234 345
2245
2246        78ˇ9 890 901
2247        "})
2248            .await;
2249        cx.simulate_shared_keystrokes("g e").await;
2250        cx.shared_state().await.assert_eq(indoc! {"
2251          123 234 345
2252          ˇ
2253          789 890 901
2254        "});
2255        cx.simulate_shared_keystrokes("g e").await;
2256        cx.shared_state().await.assert_eq(indoc! {"
2257          123 234 34ˇ5
2258
2259          789 890 901
2260        "});
2261
2262        // With punctuation
2263        cx.set_shared_state(indoc! {r"
2264        123 234 345
2265        4;5.ˇ6 567 678
2266        789 890 901
2267        "})
2268            .await;
2269        cx.simulate_shared_keystrokes("g shift-e").await;
2270        cx.shared_state().await.assert_eq(indoc! {"
2271          123 234 34ˇ5
2272          4;5.6 567 678
2273          789 890 901
2274        "});
2275    }
2276
2277    #[gpui::test]
2278    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2279        let mut cx = NeovimBackedTestContext::new(cx).await;
2280
2281        cx.set_shared_state(indoc! {"
2282            fn aˇ() {
2283              return
2284            }
2285        "})
2286            .await;
2287        cx.simulate_shared_keystrokes("v $ %").await;
2288        cx.shared_state().await.assert_eq(indoc! {"
2289            fn a«() {
2290              return
2291            }ˇ»
2292        "});
2293    }
2294}