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_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
1701    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
1702    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
1703
1704    if head > outer.start && head < inner.start {
1705        let mut offset = inner.end.to_offset(map, Bias::Left);
1706        for c in map.buffer_snapshot.chars_at(offset) {
1707            if c == '/' || c == '\n' || c == '>' {
1708                return Some(offset.to_display_point(map));
1709            }
1710            offset += c.len_utf8();
1711        }
1712    } else {
1713        let mut offset = outer.start.to_offset(map, Bias::Left);
1714        for c in map.buffer_snapshot.chars_at(offset) {
1715            offset += c.len_utf8();
1716            if c == '<' || c == '\n' {
1717                return Some(offset.to_display_point(map));
1718            }
1719        }
1720    }
1721
1722    return None;
1723}
1724
1725fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1726    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1727    let display_point = map.clip_at_line_end(display_point);
1728    let point = display_point.to_point(map);
1729    let offset = point.to_offset(&map.buffer_snapshot);
1730
1731    // Ensure the range is contained by the current line.
1732    let mut line_end = map.next_line_boundary(point).0;
1733    if line_end == point {
1734        line_end = map.max_point().to_point(map);
1735    }
1736
1737    let line_range = map.prev_line_boundary(point).0..line_end;
1738    let visible_line_range =
1739        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1740    let ranges = map
1741        .buffer_snapshot
1742        .bracket_ranges(visible_line_range.clone());
1743    if let Some(ranges) = ranges {
1744        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1745            ..line_range.end.to_offset(&map.buffer_snapshot);
1746        let mut closest_pair_destination = None;
1747        let mut closest_distance = usize::MAX;
1748
1749        for (open_range, close_range) in ranges {
1750            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
1751                if offset > open_range.start && offset < close_range.start {
1752                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1753                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
1754                        return display_point;
1755                    }
1756                    if let Some(tag) = matching_tag(map, display_point) {
1757                        return tag;
1758                    }
1759                } else if close_range.contains(&offset) {
1760                    return open_range.start.to_display_point(map);
1761                } else if open_range.contains(&offset) {
1762                    return (close_range.end - 1).to_display_point(map);
1763                }
1764            }
1765
1766            if open_range.start >= offset && line_range.contains(&open_range.start) {
1767                let distance = open_range.start - offset;
1768                if distance < closest_distance {
1769                    closest_pair_destination = Some(close_range.end - 1);
1770                    closest_distance = distance;
1771                    continue;
1772                }
1773            }
1774
1775            if close_range.start >= offset && line_range.contains(&close_range.start) {
1776                let distance = close_range.start - offset;
1777                if distance < closest_distance {
1778                    closest_pair_destination = Some(open_range.start);
1779                    closest_distance = distance;
1780                    continue;
1781                }
1782            }
1783
1784            continue;
1785        }
1786
1787        closest_pair_destination
1788            .map(|destination| destination.to_display_point(map))
1789            .unwrap_or(display_point)
1790    } else {
1791        display_point
1792    }
1793}
1794
1795fn find_forward(
1796    map: &DisplaySnapshot,
1797    from: DisplayPoint,
1798    before: bool,
1799    target: char,
1800    times: usize,
1801    mode: FindRange,
1802    smartcase: bool,
1803) -> Option<DisplayPoint> {
1804    let mut to = from;
1805    let mut found = false;
1806
1807    for _ in 0..times {
1808        found = false;
1809        let new_to = find_boundary(map, to, mode, |_, right| {
1810            found = is_character_match(target, right, smartcase);
1811            found
1812        });
1813        if to == new_to {
1814            break;
1815        }
1816        to = new_to;
1817    }
1818
1819    if found {
1820        if before && to.column() > 0 {
1821            *to.column_mut() -= 1;
1822            Some(map.clip_point(to, Bias::Left))
1823        } else {
1824            Some(to)
1825        }
1826    } else {
1827        None
1828    }
1829}
1830
1831fn find_backward(
1832    map: &DisplaySnapshot,
1833    from: DisplayPoint,
1834    after: bool,
1835    target: char,
1836    times: usize,
1837    mode: FindRange,
1838    smartcase: bool,
1839) -> DisplayPoint {
1840    let mut to = from;
1841
1842    for _ in 0..times {
1843        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1844            is_character_match(target, right, smartcase)
1845        });
1846        if to == new_to {
1847            break;
1848        }
1849        to = new_to;
1850    }
1851
1852    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1853    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1854        if after {
1855            *to.column_mut() += 1;
1856            map.clip_point(to, Bias::Right)
1857        } else {
1858            to
1859        }
1860    } else {
1861        from
1862    }
1863}
1864
1865fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1866    if smartcase {
1867        if target.is_uppercase() {
1868            target == other
1869        } else {
1870            target == other.to_ascii_lowercase()
1871        }
1872    } else {
1873        target == other
1874    }
1875}
1876
1877fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1878    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1879    first_non_whitespace(map, false, correct_line)
1880}
1881
1882fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1883    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
1884    first_non_whitespace(map, false, correct_line)
1885}
1886
1887fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1888    let correct_line = start_of_relative_buffer_row(map, point, 0);
1889    right(map, correct_line, times.saturating_sub(1))
1890}
1891
1892pub(crate) fn next_line_end(
1893    map: &DisplaySnapshot,
1894    mut point: DisplayPoint,
1895    times: usize,
1896) -> DisplayPoint {
1897    if times > 1 {
1898        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1899    }
1900    end_of_line(map, false, point, 1)
1901}
1902
1903fn window_top(
1904    map: &DisplaySnapshot,
1905    point: DisplayPoint,
1906    text_layout_details: &TextLayoutDetails,
1907    mut times: usize,
1908) -> (DisplayPoint, SelectionGoal) {
1909    let first_visible_line = text_layout_details
1910        .scroll_anchor
1911        .anchor
1912        .to_display_point(map);
1913
1914    if first_visible_line.row() != DisplayRow(0)
1915        && text_layout_details.vertical_scroll_margin as usize > times
1916    {
1917        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1918    }
1919
1920    if let Some(visible_rows) = text_layout_details.visible_rows {
1921        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1922        let new_row = (first_visible_line.row().0 + (times as u32))
1923            .min(bottom_row)
1924            .min(map.max_point().row().0);
1925        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1926
1927        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1928        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1929    } else {
1930        let new_row =
1931            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1932        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1933
1934        let new_point = DisplayPoint::new(new_row, new_col);
1935        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1936    }
1937}
1938
1939fn window_middle(
1940    map: &DisplaySnapshot,
1941    point: DisplayPoint,
1942    text_layout_details: &TextLayoutDetails,
1943) -> (DisplayPoint, SelectionGoal) {
1944    if let Some(visible_rows) = text_layout_details.visible_rows {
1945        let first_visible_line = text_layout_details
1946            .scroll_anchor
1947            .anchor
1948            .to_display_point(map);
1949
1950        let max_visible_rows =
1951            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1952
1953        let new_row =
1954            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1955        let new_row = DisplayRow(new_row);
1956        let new_col = point.column().min(map.line_len(new_row));
1957        let new_point = DisplayPoint::new(new_row, new_col);
1958        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1959    } else {
1960        (point, SelectionGoal::None)
1961    }
1962}
1963
1964fn window_bottom(
1965    map: &DisplaySnapshot,
1966    point: DisplayPoint,
1967    text_layout_details: &TextLayoutDetails,
1968    mut times: usize,
1969) -> (DisplayPoint, SelectionGoal) {
1970    if let Some(visible_rows) = text_layout_details.visible_rows {
1971        let first_visible_line = text_layout_details
1972            .scroll_anchor
1973            .anchor
1974            .to_display_point(map);
1975        let bottom_row = first_visible_line.row().0
1976            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1977        if bottom_row < map.max_point().row().0
1978            && text_layout_details.vertical_scroll_margin as usize > times
1979        {
1980            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1981        }
1982        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1983        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1984        {
1985            first_visible_line.row()
1986        } else {
1987            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1988        };
1989        let new_col = point.column().min(map.line_len(new_row));
1990        let new_point = DisplayPoint::new(new_row, new_col);
1991        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1992    } else {
1993        (point, SelectionGoal::None)
1994    }
1995}
1996
1997#[cfg(test)]
1998mod test {
1999
2000    use crate::test::NeovimBackedTestContext;
2001    use indoc::indoc;
2002
2003    #[gpui::test]
2004    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2005        let mut cx = NeovimBackedTestContext::new(cx).await;
2006
2007        let initial_state = indoc! {r"ˇabc
2008            def
2009
2010            paragraph
2011            the second
2012
2013
2014
2015            third and
2016            final"};
2017
2018        // goes down once
2019        cx.set_shared_state(initial_state).await;
2020        cx.simulate_shared_keystrokes("}").await;
2021        cx.shared_state().await.assert_eq(indoc! {r"abc
2022            def
2023            ˇ
2024            paragraph
2025            the second
2026
2027
2028
2029            third and
2030            final"});
2031
2032        // goes up once
2033        cx.simulate_shared_keystrokes("{").await;
2034        cx.shared_state().await.assert_eq(initial_state);
2035
2036        // goes down twice
2037        cx.simulate_shared_keystrokes("2 }").await;
2038        cx.shared_state().await.assert_eq(indoc! {r"abc
2039            def
2040
2041            paragraph
2042            the second
2043            ˇ
2044
2045
2046            third and
2047            final"});
2048
2049        // goes down over multiple blanks
2050        cx.simulate_shared_keystrokes("}").await;
2051        cx.shared_state().await.assert_eq(indoc! {r"abc
2052                def
2053
2054                paragraph
2055                the second
2056
2057
2058
2059                third and
2060                finaˇl"});
2061
2062        // goes up twice
2063        cx.simulate_shared_keystrokes("2 {").await;
2064        cx.shared_state().await.assert_eq(indoc! {r"abc
2065                def
2066                ˇ
2067                paragraph
2068                the second
2069
2070
2071
2072                third and
2073                final"});
2074    }
2075
2076    #[gpui::test]
2077    async fn test_matching(cx: &mut gpui::TestAppContext) {
2078        let mut cx = NeovimBackedTestContext::new(cx).await;
2079
2080        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2081                do(something(with<Types>.and_arrays[0, 2]))
2082            }"})
2083            .await;
2084        cx.simulate_shared_keystrokes("%").await;
2085        cx.shared_state()
2086            .await
2087            .assert_eq(indoc! {r"func (a stringˇ) {
2088                do(something(with<Types>.and_arrays[0, 2]))
2089            }"});
2090
2091        // test it works on the last character of the line
2092        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2093            do(something(with<Types>.and_arrays[0, 2]))
2094            }"})
2095            .await;
2096        cx.simulate_shared_keystrokes("%").await;
2097        cx.shared_state()
2098            .await
2099            .assert_eq(indoc! {r"func (a string) {
2100            do(something(with<Types>.and_arrays[0, 2]))
2101            ˇ}"});
2102
2103        // test it works on immediate nesting
2104        cx.set_shared_state("ˇ{()}").await;
2105        cx.simulate_shared_keystrokes("%").await;
2106        cx.shared_state().await.assert_eq("{()ˇ}");
2107        cx.simulate_shared_keystrokes("%").await;
2108        cx.shared_state().await.assert_eq("ˇ{()}");
2109
2110        // test it works on immediate nesting inside braces
2111        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2112        cx.simulate_shared_keystrokes("%").await;
2113        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2114
2115        // test it jumps to the next paren on a line
2116        cx.set_shared_state("func ˇboop() {\n}").await;
2117        cx.simulate_shared_keystrokes("%").await;
2118        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2119    }
2120
2121    #[gpui::test]
2122    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2123        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2124
2125        cx.neovim.exec("set filetype=html").await;
2126
2127        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2128        cx.simulate_shared_keystrokes("%").await;
2129        cx.shared_state()
2130            .await
2131            .assert_eq(indoc! {r"<body><ˇ/body>"});
2132        cx.simulate_shared_keystrokes("%").await;
2133
2134        // test jumping backwards
2135        cx.shared_state()
2136            .await
2137            .assert_eq(indoc! {r"<ˇbody></body>"});
2138
2139        // test self-closing tags
2140        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2141        cx.simulate_shared_keystrokes("%").await;
2142        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2143
2144        // test tag with attributes
2145        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2146            </div>
2147            "})
2148            .await;
2149        cx.simulate_shared_keystrokes("%").await;
2150        cx.shared_state()
2151            .await
2152            .assert_eq(indoc! {r"<div class='test' id='main'>
2153            <ˇ/div>
2154            "});
2155
2156        // test multi-line self-closing tag
2157        cx.set_shared_state(indoc! {r#"<a>
2158            <br
2159                test = "test"
2160            /ˇ>
2161        </a>"#})
2162            .await;
2163        cx.simulate_shared_keystrokes("%").await;
2164        cx.shared_state().await.assert_eq(indoc! {r#"<a>
2165            ˇ<br
2166                test = "test"
2167            />
2168        </a>"#});
2169    }
2170
2171    #[gpui::test]
2172    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2173        let mut cx = NeovimBackedTestContext::new(cx).await;
2174
2175        // f and F
2176        cx.set_shared_state("ˇone two three four").await;
2177        cx.simulate_shared_keystrokes("f o").await;
2178        cx.shared_state().await.assert_eq("one twˇo three four");
2179        cx.simulate_shared_keystrokes(",").await;
2180        cx.shared_state().await.assert_eq("ˇone two three four");
2181        cx.simulate_shared_keystrokes("2 ;").await;
2182        cx.shared_state().await.assert_eq("one two three fˇour");
2183        cx.simulate_shared_keystrokes("shift-f e").await;
2184        cx.shared_state().await.assert_eq("one two threˇe four");
2185        cx.simulate_shared_keystrokes("2 ;").await;
2186        cx.shared_state().await.assert_eq("onˇe two three four");
2187        cx.simulate_shared_keystrokes(",").await;
2188        cx.shared_state().await.assert_eq("one two thrˇee four");
2189
2190        // t and T
2191        cx.set_shared_state("ˇone two three four").await;
2192        cx.simulate_shared_keystrokes("t o").await;
2193        cx.shared_state().await.assert_eq("one tˇwo three four");
2194        cx.simulate_shared_keystrokes(",").await;
2195        cx.shared_state().await.assert_eq("oˇne two three four");
2196        cx.simulate_shared_keystrokes("2 ;").await;
2197        cx.shared_state().await.assert_eq("one two three ˇfour");
2198        cx.simulate_shared_keystrokes("shift-t e").await;
2199        cx.shared_state().await.assert_eq("one two threeˇ four");
2200        cx.simulate_shared_keystrokes("3 ;").await;
2201        cx.shared_state().await.assert_eq("oneˇ two three four");
2202        cx.simulate_shared_keystrokes(",").await;
2203        cx.shared_state().await.assert_eq("one two thˇree four");
2204    }
2205
2206    #[gpui::test]
2207    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2208        let mut cx = NeovimBackedTestContext::new(cx).await;
2209        let initial_state = indoc! {r"something(ˇfoo)"};
2210        cx.set_shared_state(initial_state).await;
2211        cx.simulate_shared_keystrokes("}").await;
2212        cx.shared_state().await.assert_eq("something(fooˇ)");
2213    }
2214
2215    #[gpui::test]
2216    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2217        let mut cx = NeovimBackedTestContext::new(cx).await;
2218        cx.set_shared_state("ˇone\n  two\nthree").await;
2219        cx.simulate_shared_keystrokes("enter").await;
2220        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
2221    }
2222
2223    #[gpui::test]
2224    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2225        let mut cx = NeovimBackedTestContext::new(cx).await;
2226        cx.set_shared_state("ˇ one\n two \nthree").await;
2227        cx.simulate_shared_keystrokes("g _").await;
2228        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2229
2230        cx.set_shared_state("ˇ one \n two \nthree").await;
2231        cx.simulate_shared_keystrokes("g _").await;
2232        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2233        cx.simulate_shared_keystrokes("2 g _").await;
2234        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2235    }
2236
2237    #[gpui::test]
2238    async fn test_window_top(cx: &mut gpui::TestAppContext) {
2239        let mut cx = NeovimBackedTestContext::new(cx).await;
2240        let initial_state = indoc! {r"abc
2241          def
2242          paragraph
2243          the second
2244          third ˇand
2245          final"};
2246
2247        cx.set_shared_state(initial_state).await;
2248        cx.simulate_shared_keystrokes("shift-h").await;
2249        cx.shared_state().await.assert_eq(indoc! {r"abˇc
2250          def
2251          paragraph
2252          the second
2253          third and
2254          final"});
2255
2256        // clip point
2257        cx.set_shared_state(indoc! {r"
2258          1 2 3
2259          4 5 6
2260          7 8 ˇ9
2261          "})
2262            .await;
2263        cx.simulate_shared_keystrokes("shift-h").await;
2264        cx.shared_state().await.assert_eq(indoc! {"
2265          1 2 ˇ3
2266          4 5 6
2267          7 8 9
2268          "});
2269
2270        cx.set_shared_state(indoc! {r"
2271          1 2 3
2272          4 5 6
2273          ˇ7 8 9
2274          "})
2275            .await;
2276        cx.simulate_shared_keystrokes("shift-h").await;
2277        cx.shared_state().await.assert_eq(indoc! {"
2278          ˇ1 2 3
2279          4 5 6
2280          7 8 9
2281          "});
2282
2283        cx.set_shared_state(indoc! {r"
2284          1 2 3
2285          4 5 ˇ6
2286          7 8 9"})
2287            .await;
2288        cx.simulate_shared_keystrokes("9 shift-h").await;
2289        cx.shared_state().await.assert_eq(indoc! {"
2290          1 2 3
2291          4 5 6
2292          7 8 ˇ9"});
2293    }
2294
2295    #[gpui::test]
2296    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2297        let mut cx = NeovimBackedTestContext::new(cx).await;
2298        let initial_state = indoc! {r"abˇc
2299          def
2300          paragraph
2301          the second
2302          third and
2303          final"};
2304
2305        cx.set_shared_state(initial_state).await;
2306        cx.simulate_shared_keystrokes("shift-m").await;
2307        cx.shared_state().await.assert_eq(indoc! {r"abc
2308          def
2309          paˇragraph
2310          the second
2311          third and
2312          final"});
2313
2314        cx.set_shared_state(indoc! {r"
2315          1 2 3
2316          4 5 6
2317          7 8 ˇ9
2318          "})
2319            .await;
2320        cx.simulate_shared_keystrokes("shift-m").await;
2321        cx.shared_state().await.assert_eq(indoc! {"
2322          1 2 3
2323          4 5 ˇ6
2324          7 8 9
2325          "});
2326        cx.set_shared_state(indoc! {r"
2327          1 2 3
2328          4 5 6
2329          ˇ7 8 9
2330          "})
2331            .await;
2332        cx.simulate_shared_keystrokes("shift-m").await;
2333        cx.shared_state().await.assert_eq(indoc! {"
2334          1 2 3
2335          ˇ4 5 6
2336          7 8 9
2337          "});
2338        cx.set_shared_state(indoc! {r"
2339          ˇ1 2 3
2340          4 5 6
2341          7 8 9
2342          "})
2343            .await;
2344        cx.simulate_shared_keystrokes("shift-m").await;
2345        cx.shared_state().await.assert_eq(indoc! {"
2346          1 2 3
2347          ˇ4 5 6
2348          7 8 9
2349          "});
2350        cx.set_shared_state(indoc! {r"
2351          1 2 3
2352          ˇ4 5 6
2353          7 8 9
2354          "})
2355            .await;
2356        cx.simulate_shared_keystrokes("shift-m").await;
2357        cx.shared_state().await.assert_eq(indoc! {"
2358          1 2 3
2359          ˇ4 5 6
2360          7 8 9
2361          "});
2362        cx.set_shared_state(indoc! {r"
2363          1 2 3
2364          4 5 ˇ6
2365          7 8 9
2366          "})
2367            .await;
2368        cx.simulate_shared_keystrokes("shift-m").await;
2369        cx.shared_state().await.assert_eq(indoc! {"
2370          1 2 3
2371          4 5 ˇ6
2372          7 8 9
2373          "});
2374    }
2375
2376    #[gpui::test]
2377    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2378        let mut cx = NeovimBackedTestContext::new(cx).await;
2379        let initial_state = indoc! {r"abc
2380          deˇf
2381          paragraph
2382          the second
2383          third and
2384          final"};
2385
2386        cx.set_shared_state(initial_state).await;
2387        cx.simulate_shared_keystrokes("shift-l").await;
2388        cx.shared_state().await.assert_eq(indoc! {r"abc
2389          def
2390          paragraph
2391          the second
2392          third and
2393          fiˇnal"});
2394
2395        cx.set_shared_state(indoc! {r"
2396          1 2 3
2397          4 5 ˇ6
2398          7 8 9
2399          "})
2400            .await;
2401        cx.simulate_shared_keystrokes("shift-l").await;
2402        cx.shared_state().await.assert_eq(indoc! {"
2403          1 2 3
2404          4 5 6
2405          7 8 9
2406          ˇ"});
2407
2408        cx.set_shared_state(indoc! {r"
2409          1 2 3
2410          ˇ4 5 6
2411          7 8 9
2412          "})
2413            .await;
2414        cx.simulate_shared_keystrokes("shift-l").await;
2415        cx.shared_state().await.assert_eq(indoc! {"
2416          1 2 3
2417          4 5 6
2418          7 8 9
2419          ˇ"});
2420
2421        cx.set_shared_state(indoc! {r"
2422          1 2 ˇ3
2423          4 5 6
2424          7 8 9
2425          "})
2426            .await;
2427        cx.simulate_shared_keystrokes("shift-l").await;
2428        cx.shared_state().await.assert_eq(indoc! {"
2429          1 2 3
2430          4 5 6
2431          7 8 9
2432          ˇ"});
2433
2434        cx.set_shared_state(indoc! {r"
2435          ˇ1 2 3
2436          4 5 6
2437          7 8 9
2438          "})
2439            .await;
2440        cx.simulate_shared_keystrokes("shift-l").await;
2441        cx.shared_state().await.assert_eq(indoc! {"
2442          1 2 3
2443          4 5 6
2444          7 8 9
2445          ˇ"});
2446
2447        cx.set_shared_state(indoc! {r"
2448          1 2 3
2449          4 5 ˇ6
2450          7 8 9
2451          "})
2452            .await;
2453        cx.simulate_shared_keystrokes("9 shift-l").await;
2454        cx.shared_state().await.assert_eq(indoc! {"
2455          1 2 ˇ3
2456          4 5 6
2457          7 8 9
2458          "});
2459    }
2460
2461    #[gpui::test]
2462    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2463        let mut cx = NeovimBackedTestContext::new(cx).await;
2464        cx.set_shared_state(indoc! {r"
2465        456 5ˇ67 678
2466        "})
2467            .await;
2468        cx.simulate_shared_keystrokes("g e").await;
2469        cx.shared_state().await.assert_eq(indoc! {"
2470        45ˇ6 567 678
2471        "});
2472
2473        // Test times
2474        cx.set_shared_state(indoc! {r"
2475        123 234 345
2476        456 5ˇ67 678
2477        "})
2478            .await;
2479        cx.simulate_shared_keystrokes("4 g e").await;
2480        cx.shared_state().await.assert_eq(indoc! {"
2481        12ˇ3 234 345
2482        456 567 678
2483        "});
2484
2485        // With punctuation
2486        cx.set_shared_state(indoc! {r"
2487        123 234 345
2488        4;5.6 5ˇ67 678
2489        789 890 901
2490        "})
2491            .await;
2492        cx.simulate_shared_keystrokes("g e").await;
2493        cx.shared_state().await.assert_eq(indoc! {"
2494          123 234 345
2495          4;5.ˇ6 567 678
2496          789 890 901
2497        "});
2498
2499        // With punctuation and count
2500        cx.set_shared_state(indoc! {r"
2501        123 234 345
2502        4;5.6 5ˇ67 678
2503        789 890 901
2504        "})
2505            .await;
2506        cx.simulate_shared_keystrokes("5 g e").await;
2507        cx.shared_state().await.assert_eq(indoc! {"
2508          123 234 345
2509          ˇ4;5.6 567 678
2510          789 890 901
2511        "});
2512
2513        // newlines
2514        cx.set_shared_state(indoc! {r"
2515        123 234 345
2516
2517        78ˇ9 890 901
2518        "})
2519            .await;
2520        cx.simulate_shared_keystrokes("g e").await;
2521        cx.shared_state().await.assert_eq(indoc! {"
2522          123 234 345
2523          ˇ
2524          789 890 901
2525        "});
2526        cx.simulate_shared_keystrokes("g e").await;
2527        cx.shared_state().await.assert_eq(indoc! {"
2528          123 234 34ˇ5
2529
2530          789 890 901
2531        "});
2532
2533        // With punctuation
2534        cx.set_shared_state(indoc! {r"
2535        123 234 345
2536        4;5.ˇ6 567 678
2537        789 890 901
2538        "})
2539            .await;
2540        cx.simulate_shared_keystrokes("g shift-e").await;
2541        cx.shared_state().await.assert_eq(indoc! {"
2542          123 234 34ˇ5
2543          4;5.6 567 678
2544          789 890 901
2545        "});
2546    }
2547
2548    #[gpui::test]
2549    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2550        let mut cx = NeovimBackedTestContext::new(cx).await;
2551
2552        cx.set_shared_state(indoc! {"
2553            fn aˇ() {
2554              return
2555            }
2556        "})
2557            .await;
2558        cx.simulate_shared_keystrokes("v $ %").await;
2559        cx.shared_state().await.assert_eq(indoc! {"
2560            fn a«() {
2561              return
2562            }ˇ»
2563        "});
2564    }
2565}