motion.rs

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