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