motion.rs

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