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                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().0 - begin_folded_line.row().0;
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().next_row(), 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(MultiBufferRow(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(MultiBufferRow(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
1443    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1444    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1445        if char_kind(&scope, ch) != CharKind::Whitespace {
1446            return end_of_line.to_display_point(map);
1447        }
1448    }
1449
1450    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1451        if ch == '\n' {
1452            break;
1453        }
1454        end_of_line = offset;
1455        if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
1456            break;
1457        }
1458    }
1459
1460    end_of_line.to_display_point(map)
1461}
1462
1463pub(crate) fn start_of_line(
1464    map: &DisplaySnapshot,
1465    display_lines: bool,
1466    point: DisplayPoint,
1467) -> DisplayPoint {
1468    if display_lines {
1469        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1470    } else {
1471        map.prev_line_boundary(point.to_point(map)).1
1472    }
1473}
1474
1475pub(crate) fn end_of_line(
1476    map: &DisplaySnapshot,
1477    display_lines: bool,
1478    mut point: DisplayPoint,
1479    times: usize,
1480) -> DisplayPoint {
1481    if times > 1 {
1482        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1483    }
1484    if display_lines {
1485        map.clip_point(
1486            DisplayPoint::new(point.row(), map.line_len(point.row())),
1487            Bias::Left,
1488        )
1489    } else {
1490        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1491    }
1492}
1493
1494fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1495    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1496    *new_point.column_mut() = point.column();
1497    map.clip_point(new_point, Bias::Left)
1498}
1499
1500fn end_of_document(
1501    map: &DisplaySnapshot,
1502    point: DisplayPoint,
1503    line: Option<usize>,
1504) -> DisplayPoint {
1505    let new_row = if let Some(line) = line {
1506        (line - 1) as u32
1507    } else {
1508        map.max_buffer_row().0
1509    };
1510
1511    let new_point = Point::new(new_row, point.column());
1512    map.clip_point(new_point.to_display_point(map), Bias::Left)
1513}
1514
1515fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1516    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1517    let display_point = map.clip_at_line_end(display_point);
1518    let point = display_point.to_point(map);
1519    let offset = point.to_offset(&map.buffer_snapshot);
1520
1521    // Ensure the range is contained by the current line.
1522    let mut line_end = map.next_line_boundary(point).0;
1523    if line_end == point {
1524        line_end = map.max_point().to_point(map);
1525    }
1526
1527    let line_range = map.prev_line_boundary(point).0..line_end;
1528    let visible_line_range =
1529        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1530    let ranges = map
1531        .buffer_snapshot
1532        .bracket_ranges(visible_line_range.clone());
1533    if let Some(ranges) = ranges {
1534        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1535            ..line_range.end.to_offset(&map.buffer_snapshot);
1536        let mut closest_pair_destination = None;
1537        let mut closest_distance = usize::MAX;
1538
1539        for (open_range, close_range) in ranges {
1540            if open_range.start >= offset && line_range.contains(&open_range.start) {
1541                let distance = open_range.start - offset;
1542                if distance < closest_distance {
1543                    closest_pair_destination = Some(close_range.start);
1544                    closest_distance = distance;
1545                    continue;
1546                }
1547            }
1548
1549            if close_range.start >= offset && line_range.contains(&close_range.start) {
1550                let distance = close_range.start - offset;
1551                if distance < closest_distance {
1552                    closest_pair_destination = Some(open_range.start);
1553                    closest_distance = distance;
1554                    continue;
1555                }
1556            }
1557
1558            continue;
1559        }
1560
1561        closest_pair_destination
1562            .map(|destination| destination.to_display_point(map))
1563            .unwrap_or(display_point)
1564    } else {
1565        display_point
1566    }
1567}
1568
1569fn find_forward(
1570    map: &DisplaySnapshot,
1571    from: DisplayPoint,
1572    before: bool,
1573    target: char,
1574    times: usize,
1575    mode: FindRange,
1576    smartcase: bool,
1577) -> Option<DisplayPoint> {
1578    let mut to = from;
1579    let mut found = false;
1580
1581    for _ in 0..times {
1582        found = false;
1583        let new_to = find_boundary(map, to, mode, |_, right| {
1584            found = is_character_match(target, right, smartcase);
1585            found
1586        });
1587        if to == new_to {
1588            break;
1589        }
1590        to = new_to;
1591    }
1592
1593    if found {
1594        if before && to.column() > 0 {
1595            *to.column_mut() -= 1;
1596            Some(map.clip_point(to, Bias::Left))
1597        } else {
1598            Some(to)
1599        }
1600    } else {
1601        None
1602    }
1603}
1604
1605fn find_backward(
1606    map: &DisplaySnapshot,
1607    from: DisplayPoint,
1608    after: bool,
1609    target: char,
1610    times: usize,
1611    mode: FindRange,
1612    smartcase: bool,
1613) -> DisplayPoint {
1614    let mut to = from;
1615
1616    for _ in 0..times {
1617        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1618            is_character_match(target, right, smartcase)
1619        });
1620        if to == new_to {
1621            break;
1622        }
1623        to = new_to;
1624    }
1625
1626    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1627    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1628        if after {
1629            *to.column_mut() += 1;
1630            map.clip_point(to, Bias::Right)
1631        } else {
1632            to
1633        }
1634    } else {
1635        from
1636    }
1637}
1638
1639fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1640    if smartcase {
1641        if target.is_uppercase() {
1642            target == other
1643        } else {
1644            target == other.to_ascii_lowercase()
1645        }
1646    } else {
1647        target == other
1648    }
1649}
1650
1651fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1652    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1653    first_non_whitespace(map, false, correct_line)
1654}
1655
1656fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1657    let correct_line = start_of_relative_buffer_row(map, point, 0);
1658    right(map, correct_line, times.saturating_sub(1))
1659}
1660
1661pub(crate) fn next_line_end(
1662    map: &DisplaySnapshot,
1663    mut point: DisplayPoint,
1664    times: usize,
1665) -> DisplayPoint {
1666    if times > 1 {
1667        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1668    }
1669    end_of_line(map, false, point, 1)
1670}
1671
1672fn window_top(
1673    map: &DisplaySnapshot,
1674    point: DisplayPoint,
1675    text_layout_details: &TextLayoutDetails,
1676    mut times: usize,
1677) -> (DisplayPoint, SelectionGoal) {
1678    let first_visible_line = text_layout_details
1679        .scroll_anchor
1680        .anchor
1681        .to_display_point(map);
1682
1683    if first_visible_line.row() != DisplayRow(0)
1684        && text_layout_details.vertical_scroll_margin as usize > times
1685    {
1686        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1687    }
1688
1689    if let Some(visible_rows) = text_layout_details.visible_rows {
1690        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1691        let new_row = (first_visible_line.row().0 + (times as u32))
1692            .min(bottom_row)
1693            .min(map.max_point().row().0);
1694        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1695
1696        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1697        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1698    } else {
1699        let new_row =
1700            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1701        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1702
1703        let new_point = DisplayPoint::new(new_row, new_col);
1704        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1705    }
1706}
1707
1708fn window_middle(
1709    map: &DisplaySnapshot,
1710    point: DisplayPoint,
1711    text_layout_details: &TextLayoutDetails,
1712) -> (DisplayPoint, SelectionGoal) {
1713    if let Some(visible_rows) = text_layout_details.visible_rows {
1714        let first_visible_line = text_layout_details
1715            .scroll_anchor
1716            .anchor
1717            .to_display_point(map);
1718
1719        let max_visible_rows =
1720            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1721
1722        let new_row =
1723            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1724        let new_row = DisplayRow(new_row);
1725        let new_col = point.column().min(map.line_len(new_row));
1726        let new_point = DisplayPoint::new(new_row, new_col);
1727        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1728    } else {
1729        (point, SelectionGoal::None)
1730    }
1731}
1732
1733fn window_bottom(
1734    map: &DisplaySnapshot,
1735    point: DisplayPoint,
1736    text_layout_details: &TextLayoutDetails,
1737    mut times: usize,
1738) -> (DisplayPoint, SelectionGoal) {
1739    if let Some(visible_rows) = text_layout_details.visible_rows {
1740        let first_visible_line = text_layout_details
1741            .scroll_anchor
1742            .anchor
1743            .to_display_point(map);
1744        let bottom_row = first_visible_line.row().0
1745            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1746        if bottom_row < map.max_point().row().0
1747            && text_layout_details.vertical_scroll_margin as usize > times
1748        {
1749            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1750        }
1751        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1752        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1753        {
1754            first_visible_line.row()
1755        } else {
1756            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1757        };
1758        let new_col = point.column().min(map.line_len(new_row));
1759        let new_point = DisplayPoint::new(new_row, new_col);
1760        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1761    } else {
1762        (point, SelectionGoal::None)
1763    }
1764}
1765
1766pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind {
1767    if treat_punctuation_as_word && kind == CharKind::Punctuation {
1768        CharKind::Word
1769    } else {
1770        kind
1771    }
1772}
1773
1774#[cfg(test)]
1775mod test {
1776
1777    use crate::test::NeovimBackedTestContext;
1778    use indoc::indoc;
1779
1780    #[gpui::test]
1781    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1782        let mut cx = NeovimBackedTestContext::new(cx).await;
1783
1784        let initial_state = indoc! {r"ˇabc
1785            def
1786
1787            paragraph
1788            the second
1789
1790
1791
1792            third and
1793            final"};
1794
1795        // goes down once
1796        cx.set_shared_state(initial_state).await;
1797        cx.simulate_shared_keystrokes("}").await;
1798        cx.shared_state().await.assert_eq(indoc! {r"abc
1799            def
1800            ˇ
1801            paragraph
1802            the second
1803
1804
1805
1806            third and
1807            final"});
1808
1809        // goes up once
1810        cx.simulate_shared_keystrokes("{").await;
1811        cx.shared_state().await.assert_eq(initial_state);
1812
1813        // goes down twice
1814        cx.simulate_shared_keystrokes("2 }").await;
1815        cx.shared_state().await.assert_eq(indoc! {r"abc
1816            def
1817
1818            paragraph
1819            the second
1820            ˇ
1821
1822
1823            third and
1824            final"});
1825
1826        // goes down over multiple blanks
1827        cx.simulate_shared_keystrokes("}").await;
1828        cx.shared_state().await.assert_eq(indoc! {r"abc
1829                def
1830
1831                paragraph
1832                the second
1833
1834
1835
1836                third and
1837                finaˇl"});
1838
1839        // goes up twice
1840        cx.simulate_shared_keystrokes("2 {").await;
1841        cx.shared_state().await.assert_eq(indoc! {r"abc
1842                def
1843                ˇ
1844                paragraph
1845                the second
1846
1847
1848
1849                third and
1850                final"});
1851    }
1852
1853    #[gpui::test]
1854    async fn test_matching(cx: &mut gpui::TestAppContext) {
1855        let mut cx = NeovimBackedTestContext::new(cx).await;
1856
1857        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1858                do(something(with<Types>.and_arrays[0, 2]))
1859            }"})
1860            .await;
1861        cx.simulate_shared_keystrokes("%").await;
1862        cx.shared_state()
1863            .await
1864            .assert_eq(indoc! {r"func (a stringˇ) {
1865                do(something(with<Types>.and_arrays[0, 2]))
1866            }"});
1867
1868        // test it works on the last character of the line
1869        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1870            do(something(with<Types>.and_arrays[0, 2]))
1871            }"})
1872            .await;
1873        cx.simulate_shared_keystrokes("%").await;
1874        cx.shared_state()
1875            .await
1876            .assert_eq(indoc! {r"func (a string) {
1877            do(something(with<Types>.and_arrays[0, 2]))
1878            ˇ}"});
1879
1880        // test it works on immediate nesting
1881        cx.set_shared_state("ˇ{()}").await;
1882        cx.simulate_shared_keystrokes("%").await;
1883        cx.shared_state().await.assert_eq("{()ˇ}");
1884        cx.simulate_shared_keystrokes("%").await;
1885        cx.shared_state().await.assert_eq("ˇ{()}");
1886
1887        // test it works on immediate nesting inside braces
1888        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1889        cx.simulate_shared_keystrokes("%").await;
1890        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
1891
1892        // test it jumps to the next paren on a line
1893        cx.set_shared_state("func ˇboop() {\n}").await;
1894        cx.simulate_shared_keystrokes("%").await;
1895        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
1896    }
1897
1898    #[gpui::test]
1899    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1900        let mut cx = NeovimBackedTestContext::new(cx).await;
1901
1902        // f and F
1903        cx.set_shared_state("ˇone two three four").await;
1904        cx.simulate_shared_keystrokes("f o").await;
1905        cx.shared_state().await.assert_eq("one twˇo three four");
1906        cx.simulate_shared_keystrokes(",").await;
1907        cx.shared_state().await.assert_eq("ˇone two three four");
1908        cx.simulate_shared_keystrokes("2 ;").await;
1909        cx.shared_state().await.assert_eq("one two three fˇour");
1910        cx.simulate_shared_keystrokes("shift-f e").await;
1911        cx.shared_state().await.assert_eq("one two threˇe four");
1912        cx.simulate_shared_keystrokes("2 ;").await;
1913        cx.shared_state().await.assert_eq("onˇe two three four");
1914        cx.simulate_shared_keystrokes(",").await;
1915        cx.shared_state().await.assert_eq("one two thrˇee four");
1916
1917        // t and T
1918        cx.set_shared_state("ˇone two three four").await;
1919        cx.simulate_shared_keystrokes("t o").await;
1920        cx.shared_state().await.assert_eq("one tˇwo three four");
1921        cx.simulate_shared_keystrokes(",").await;
1922        cx.shared_state().await.assert_eq("oˇne two three four");
1923        cx.simulate_shared_keystrokes("2 ;").await;
1924        cx.shared_state().await.assert_eq("one two three ˇfour");
1925        cx.simulate_shared_keystrokes("shift-t e").await;
1926        cx.shared_state().await.assert_eq("one two threeˇ four");
1927        cx.simulate_shared_keystrokes("3 ;").await;
1928        cx.shared_state().await.assert_eq("oneˇ two three four");
1929        cx.simulate_shared_keystrokes(",").await;
1930        cx.shared_state().await.assert_eq("one two thˇree four");
1931    }
1932
1933    #[gpui::test]
1934    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1935        let mut cx = NeovimBackedTestContext::new(cx).await;
1936        let initial_state = indoc! {r"something(ˇfoo)"};
1937        cx.set_shared_state(initial_state).await;
1938        cx.simulate_shared_keystrokes("}").await;
1939        cx.shared_state().await.assert_eq("something(fooˇ)");
1940    }
1941
1942    #[gpui::test]
1943    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1944        let mut cx = NeovimBackedTestContext::new(cx).await;
1945        cx.set_shared_state("ˇone\n  two\nthree").await;
1946        cx.simulate_shared_keystrokes("enter").await;
1947        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
1948    }
1949
1950    #[gpui::test]
1951    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1952        let mut cx = NeovimBackedTestContext::new(cx).await;
1953        cx.set_shared_state("ˇ one\n two \nthree").await;
1954        cx.simulate_shared_keystrokes("g _").await;
1955        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
1956
1957        cx.set_shared_state("ˇ one \n two \nthree").await;
1958        cx.simulate_shared_keystrokes("g _").await;
1959        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
1960        cx.simulate_shared_keystrokes("2 g _").await;
1961        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
1962    }
1963
1964    #[gpui::test]
1965    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1966        let mut cx = NeovimBackedTestContext::new(cx).await;
1967        let initial_state = indoc! {r"abc
1968          def
1969          paragraph
1970          the second
1971          third ˇand
1972          final"};
1973
1974        cx.set_shared_state(initial_state).await;
1975        cx.simulate_shared_keystrokes("shift-h").await;
1976        cx.shared_state().await.assert_eq(indoc! {r"abˇc
1977          def
1978          paragraph
1979          the second
1980          third and
1981          final"});
1982
1983        // clip point
1984        cx.set_shared_state(indoc! {r"
1985          1 2 3
1986          4 5 6
1987          7 8 ˇ9
1988          "})
1989            .await;
1990        cx.simulate_shared_keystrokes("shift-h").await;
1991        cx.shared_state().await.assert_eq(indoc! {"
1992          1 2 ˇ3
1993          4 5 6
1994          7 8 9
1995          "});
1996
1997        cx.set_shared_state(indoc! {r"
1998          1 2 3
1999          4 5 6
2000          ˇ7 8 9
2001          "})
2002            .await;
2003        cx.simulate_shared_keystrokes("shift-h").await;
2004        cx.shared_state().await.assert_eq(indoc! {"
2005          ˇ1 2 3
2006          4 5 6
2007          7 8 9
2008          "});
2009
2010        cx.set_shared_state(indoc! {r"
2011          1 2 3
2012          4 5 ˇ6
2013          7 8 9"})
2014            .await;
2015        cx.simulate_shared_keystrokes("9 shift-h").await;
2016        cx.shared_state().await.assert_eq(indoc! {"
2017          1 2 3
2018          4 5 6
2019          7 8 ˇ9"});
2020    }
2021
2022    #[gpui::test]
2023    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2024        let mut cx = NeovimBackedTestContext::new(cx).await;
2025        let initial_state = indoc! {r"abˇc
2026          def
2027          paragraph
2028          the second
2029          third and
2030          final"};
2031
2032        cx.set_shared_state(initial_state).await;
2033        cx.simulate_shared_keystrokes("shift-m").await;
2034        cx.shared_state().await.assert_eq(indoc! {r"abc
2035          def
2036          paˇragraph
2037          the second
2038          third and
2039          final"});
2040
2041        cx.set_shared_state(indoc! {r"
2042          1 2 3
2043          4 5 6
2044          7 8 ˇ9
2045          "})
2046            .await;
2047        cx.simulate_shared_keystrokes("shift-m").await;
2048        cx.shared_state().await.assert_eq(indoc! {"
2049          1 2 3
2050          4 5 ˇ6
2051          7 8 9
2052          "});
2053        cx.set_shared_state(indoc! {r"
2054          1 2 3
2055          4 5 6
2056          ˇ7 8 9
2057          "})
2058            .await;
2059        cx.simulate_shared_keystrokes("shift-m").await;
2060        cx.shared_state().await.assert_eq(indoc! {"
2061          1 2 3
2062          ˇ4 5 6
2063          7 8 9
2064          "});
2065        cx.set_shared_state(indoc! {r"
2066          ˇ1 2 3
2067          4 5 6
2068          7 8 9
2069          "})
2070            .await;
2071        cx.simulate_shared_keystrokes("shift-m").await;
2072        cx.shared_state().await.assert_eq(indoc! {"
2073          1 2 3
2074          ˇ4 5 6
2075          7 8 9
2076          "});
2077        cx.set_shared_state(indoc! {r"
2078          1 2 3
2079          ˇ4 5 6
2080          7 8 9
2081          "})
2082            .await;
2083        cx.simulate_shared_keystrokes("shift-m").await;
2084        cx.shared_state().await.assert_eq(indoc! {"
2085          1 2 3
2086          ˇ4 5 6
2087          7 8 9
2088          "});
2089        cx.set_shared_state(indoc! {r"
2090          1 2 3
2091          4 5 ˇ6
2092          7 8 9
2093          "})
2094            .await;
2095        cx.simulate_shared_keystrokes("shift-m").await;
2096        cx.shared_state().await.assert_eq(indoc! {"
2097          1 2 3
2098          4 5 ˇ6
2099          7 8 9
2100          "});
2101    }
2102
2103    #[gpui::test]
2104    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2105        let mut cx = NeovimBackedTestContext::new(cx).await;
2106        let initial_state = indoc! {r"abc
2107          deˇf
2108          paragraph
2109          the second
2110          third and
2111          final"};
2112
2113        cx.set_shared_state(initial_state).await;
2114        cx.simulate_shared_keystrokes("shift-l").await;
2115        cx.shared_state().await.assert_eq(indoc! {r"abc
2116          def
2117          paragraph
2118          the second
2119          third and
2120          fiˇnal"});
2121
2122        cx.set_shared_state(indoc! {r"
2123          1 2 3
2124          4 5 ˇ6
2125          7 8 9
2126          "})
2127            .await;
2128        cx.simulate_shared_keystrokes("shift-l").await;
2129        cx.shared_state().await.assert_eq(indoc! {"
2130          1 2 3
2131          4 5 6
2132          7 8 9
2133          ˇ"});
2134
2135        cx.set_shared_state(indoc! {r"
2136          1 2 3
2137          ˇ4 5 6
2138          7 8 9
2139          "})
2140            .await;
2141        cx.simulate_shared_keystrokes("shift-l").await;
2142        cx.shared_state().await.assert_eq(indoc! {"
2143          1 2 3
2144          4 5 6
2145          7 8 9
2146          ˇ"});
2147
2148        cx.set_shared_state(indoc! {r"
2149          1 2 ˇ3
2150          4 5 6
2151          7 8 9
2152          "})
2153            .await;
2154        cx.simulate_shared_keystrokes("shift-l").await;
2155        cx.shared_state().await.assert_eq(indoc! {"
2156          1 2 3
2157          4 5 6
2158          7 8 9
2159          ˇ"});
2160
2161        cx.set_shared_state(indoc! {r"
2162          ˇ1 2 3
2163          4 5 6
2164          7 8 9
2165          "})
2166            .await;
2167        cx.simulate_shared_keystrokes("shift-l").await;
2168        cx.shared_state().await.assert_eq(indoc! {"
2169          1 2 3
2170          4 5 6
2171          7 8 9
2172          ˇ"});
2173
2174        cx.set_shared_state(indoc! {r"
2175          1 2 3
2176          4 5 ˇ6
2177          7 8 9
2178          "})
2179            .await;
2180        cx.simulate_shared_keystrokes("9 shift-l").await;
2181        cx.shared_state().await.assert_eq(indoc! {"
2182          1 2 ˇ3
2183          4 5 6
2184          7 8 9
2185          "});
2186    }
2187
2188    #[gpui::test]
2189    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2190        let mut cx = NeovimBackedTestContext::new(cx).await;
2191        cx.set_shared_state(indoc! {r"
2192        456 5ˇ67 678
2193        "})
2194            .await;
2195        cx.simulate_shared_keystrokes("g e").await;
2196        cx.shared_state().await.assert_eq(indoc! {"
2197        45ˇ6 567 678
2198        "});
2199
2200        // Test times
2201        cx.set_shared_state(indoc! {r"
2202        123 234 345
2203        456 5ˇ67 678
2204        "})
2205            .await;
2206        cx.simulate_shared_keystrokes("4 g e").await;
2207        cx.shared_state().await.assert_eq(indoc! {"
2208        12ˇ3 234 345
2209        456 567 678
2210        "});
2211
2212        // With punctuation
2213        cx.set_shared_state(indoc! {r"
2214        123 234 345
2215        4;5.6 5ˇ67 678
2216        789 890 901
2217        "})
2218            .await;
2219        cx.simulate_shared_keystrokes("g e").await;
2220        cx.shared_state().await.assert_eq(indoc! {"
2221          123 234 345
2222          4;5.ˇ6 567 678
2223          789 890 901
2224        "});
2225
2226        // With punctuation and count
2227        cx.set_shared_state(indoc! {r"
2228        123 234 345
2229        4;5.6 5ˇ67 678
2230        789 890 901
2231        "})
2232            .await;
2233        cx.simulate_shared_keystrokes("5 g e").await;
2234        cx.shared_state().await.assert_eq(indoc! {"
2235          123 234 345
2236          ˇ4;5.6 567 678
2237          789 890 901
2238        "});
2239
2240        // newlines
2241        cx.set_shared_state(indoc! {r"
2242        123 234 345
2243
2244        78ˇ9 890 901
2245        "})
2246            .await;
2247        cx.simulate_shared_keystrokes("g e").await;
2248        cx.shared_state().await.assert_eq(indoc! {"
2249          123 234 345
2250          ˇ
2251          789 890 901
2252        "});
2253        cx.simulate_shared_keystrokes("g e").await;
2254        cx.shared_state().await.assert_eq(indoc! {"
2255          123 234 34ˇ5
2256
2257          789 890 901
2258        "});
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.shared_state().await.assert_eq(indoc! {"
2269          123 234 34ˇ5
2270          4;5.6 567 678
2271          789 890 901
2272        "});
2273    }
2274
2275    #[gpui::test]
2276    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2277        let mut cx = NeovimBackedTestContext::new(cx).await;
2278
2279        cx.set_shared_state(indoc! {"
2280            fn aˇ() {
2281              return
2282            }
2283        "})
2284            .await;
2285        cx.simulate_shared_keystrokes("v $ %").await;
2286        cx.shared_state().await.assert_eq(indoc! {"
2287            fn a«() {
2288              return
2289            }ˇ»
2290        "});
2291    }
2292}