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 start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1287    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1288    for (ch, offset) in map.buffer_chars_at(start_offset) {
1289        if ch == '\n' {
1290            return from;
1291        }
1292
1293        start_offset = offset;
1294
1295        if char_kind(&scope, ch) != CharKind::Whitespace {
1296            break;
1297        }
1298    }
1299
1300    start_offset.to_display_point(map)
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 display_point = map.clip_at_line_end(display_point);
1358    let point = display_point.to_point(map);
1359    let offset = point.to_offset(&map.buffer_snapshot);
1360
1361    // Ensure the range is contained by the current line.
1362    let mut line_end = map.next_line_boundary(point).0;
1363    if line_end == point {
1364        line_end = map.max_point().to_point(map);
1365    }
1366
1367    let line_range = map.prev_line_boundary(point).0..line_end;
1368    let visible_line_range =
1369        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1370    let ranges = map
1371        .buffer_snapshot
1372        .bracket_ranges(visible_line_range.clone());
1373    if let Some(ranges) = ranges {
1374        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1375            ..line_range.end.to_offset(&map.buffer_snapshot);
1376        let mut closest_pair_destination = None;
1377        let mut closest_distance = usize::MAX;
1378
1379        for (open_range, close_range) in ranges {
1380            if open_range.start >= offset && line_range.contains(&open_range.start) {
1381                let distance = open_range.start - offset;
1382                if distance < closest_distance {
1383                    closest_pair_destination = Some(close_range.start);
1384                    closest_distance = distance;
1385                    continue;
1386                }
1387            }
1388
1389            if close_range.start >= offset && line_range.contains(&close_range.start) {
1390                let distance = close_range.start - offset;
1391                if distance < closest_distance {
1392                    closest_pair_destination = Some(open_range.start);
1393                    closest_distance = distance;
1394                    continue;
1395                }
1396            }
1397
1398            continue;
1399        }
1400
1401        closest_pair_destination
1402            .map(|destination| destination.to_display_point(map))
1403            .unwrap_or(display_point)
1404    } else {
1405        display_point
1406    }
1407}
1408
1409fn find_forward(
1410    map: &DisplaySnapshot,
1411    from: DisplayPoint,
1412    before: bool,
1413    target: char,
1414    times: usize,
1415    mode: FindRange,
1416    smartcase: bool,
1417) -> Option<DisplayPoint> {
1418    let mut to = from;
1419    let mut found = false;
1420
1421    for _ in 0..times {
1422        found = false;
1423        let new_to = find_boundary(map, to, mode, |_, right| {
1424            found = is_character_match(target, right, smartcase);
1425            found
1426        });
1427        if to == new_to {
1428            break;
1429        }
1430        to = new_to;
1431    }
1432
1433    if found {
1434        if before && to.column() > 0 {
1435            *to.column_mut() -= 1;
1436            Some(map.clip_point(to, Bias::Left))
1437        } else {
1438            Some(to)
1439        }
1440    } else {
1441        None
1442    }
1443}
1444
1445fn find_backward(
1446    map: &DisplaySnapshot,
1447    from: DisplayPoint,
1448    after: bool,
1449    target: char,
1450    times: usize,
1451    mode: FindRange,
1452    smartcase: bool,
1453) -> DisplayPoint {
1454    let mut to = from;
1455
1456    for _ in 0..times {
1457        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1458            is_character_match(target, right, smartcase)
1459        });
1460        if to == new_to {
1461            break;
1462        }
1463        to = new_to;
1464    }
1465
1466    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1467    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1468        if after {
1469            *to.column_mut() += 1;
1470            map.clip_point(to, Bias::Right)
1471        } else {
1472            to
1473        }
1474    } else {
1475        from
1476    }
1477}
1478
1479fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1480    if smartcase {
1481        if target.is_uppercase() {
1482            target == other
1483        } else {
1484            target == other.to_ascii_lowercase()
1485        }
1486    } else {
1487        target == other
1488    }
1489}
1490
1491fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1492    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1493    first_non_whitespace(map, false, correct_line)
1494}
1495
1496fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1497    let correct_line = start_of_relative_buffer_row(map, point, 0);
1498    right(map, correct_line, times.saturating_sub(1))
1499}
1500
1501pub(crate) fn next_line_end(
1502    map: &DisplaySnapshot,
1503    mut point: DisplayPoint,
1504    times: usize,
1505) -> DisplayPoint {
1506    if times > 1 {
1507        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1508    }
1509    end_of_line(map, false, point, 1)
1510}
1511
1512fn window_top(
1513    map: &DisplaySnapshot,
1514    point: DisplayPoint,
1515    text_layout_details: &TextLayoutDetails,
1516    mut times: usize,
1517) -> (DisplayPoint, SelectionGoal) {
1518    let first_visible_line = text_layout_details
1519        .scroll_anchor
1520        .anchor
1521        .to_display_point(map);
1522
1523    if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1524    {
1525        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1526    }
1527
1528    if let Some(visible_rows) = text_layout_details.visible_rows {
1529        let bottom_row = first_visible_line.row() + visible_rows as u32;
1530        let new_row = (first_visible_line.row() + (times as u32))
1531            .min(bottom_row)
1532            .min(map.max_point().row());
1533        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1534
1535        let new_point = DisplayPoint::new(new_row, new_col);
1536        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1537    } else {
1538        let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1539        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1540
1541        let new_point = DisplayPoint::new(new_row, new_col);
1542        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1543    }
1544}
1545
1546fn window_middle(
1547    map: &DisplaySnapshot,
1548    point: DisplayPoint,
1549    text_layout_details: &TextLayoutDetails,
1550) -> (DisplayPoint, SelectionGoal) {
1551    if let Some(visible_rows) = text_layout_details.visible_rows {
1552        let first_visible_line = text_layout_details
1553            .scroll_anchor
1554            .anchor
1555            .to_display_point(map);
1556
1557        let max_visible_rows =
1558            (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1559
1560        let new_row =
1561            (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1562        let new_col = point.column().min(map.line_len(new_row));
1563        let new_point = DisplayPoint::new(new_row, new_col);
1564        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1565    } else {
1566        (point, SelectionGoal::None)
1567    }
1568}
1569
1570fn window_bottom(
1571    map: &DisplaySnapshot,
1572    point: DisplayPoint,
1573    text_layout_details: &TextLayoutDetails,
1574    mut times: usize,
1575) -> (DisplayPoint, SelectionGoal) {
1576    if let Some(visible_rows) = text_layout_details.visible_rows {
1577        let first_visible_line = text_layout_details
1578            .scroll_anchor
1579            .anchor
1580            .to_display_point(map);
1581        let bottom_row = first_visible_line.row()
1582            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1583        if bottom_row < map.max_point().row()
1584            && text_layout_details.vertical_scroll_margin as usize > times
1585        {
1586            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1587        }
1588        let bottom_row_capped = bottom_row.min(map.max_point().row());
1589        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1590            first_visible_line.row()
1591        } else {
1592            bottom_row_capped.saturating_sub(times as u32)
1593        };
1594        let new_col = point.column().min(map.line_len(new_row));
1595        let new_point = DisplayPoint::new(new_row, new_col);
1596        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1597    } else {
1598        (point, SelectionGoal::None)
1599    }
1600}
1601
1602#[cfg(test)]
1603mod test {
1604
1605    use crate::test::NeovimBackedTestContext;
1606    use indoc::indoc;
1607
1608    #[gpui::test]
1609    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1610        let mut cx = NeovimBackedTestContext::new(cx).await;
1611
1612        let initial_state = indoc! {r"ˇabc
1613            def
1614
1615            paragraph
1616            the second
1617
1618
1619
1620            third and
1621            final"};
1622
1623        // goes down once
1624        cx.set_shared_state(initial_state).await;
1625        cx.simulate_shared_keystrokes(["}"]).await;
1626        cx.assert_shared_state(indoc! {r"abc
1627            def
1628            ˇ
1629            paragraph
1630            the second
1631
1632
1633
1634            third and
1635            final"})
1636            .await;
1637
1638        // goes up once
1639        cx.simulate_shared_keystrokes(["{"]).await;
1640        cx.assert_shared_state(initial_state).await;
1641
1642        // goes down twice
1643        cx.simulate_shared_keystrokes(["2", "}"]).await;
1644        cx.assert_shared_state(indoc! {r"abc
1645            def
1646
1647            paragraph
1648            the second
1649            ˇ
1650
1651
1652            third and
1653            final"})
1654            .await;
1655
1656        // goes down over multiple blanks
1657        cx.simulate_shared_keystrokes(["}"]).await;
1658        cx.assert_shared_state(indoc! {r"abc
1659                def
1660
1661                paragraph
1662                the second
1663
1664
1665
1666                third and
1667                finaˇl"})
1668            .await;
1669
1670        // goes up twice
1671        cx.simulate_shared_keystrokes(["2", "{"]).await;
1672        cx.assert_shared_state(indoc! {r"abc
1673                def
1674                ˇ
1675                paragraph
1676                the second
1677
1678
1679
1680                third and
1681                final"})
1682            .await
1683    }
1684
1685    #[gpui::test]
1686    async fn test_matching(cx: &mut gpui::TestAppContext) {
1687        let mut cx = NeovimBackedTestContext::new(cx).await;
1688
1689        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1690                do(something(with<Types>.and_arrays[0, 2]))
1691            }"})
1692            .await;
1693        cx.simulate_shared_keystrokes(["%"]).await;
1694        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1695                do(something(with<Types>.and_arrays[0, 2]))
1696            }"})
1697            .await;
1698
1699        // test it works on the last character of the line
1700        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1701            do(something(with<Types>.and_arrays[0, 2]))
1702            }"})
1703            .await;
1704        cx.simulate_shared_keystrokes(["%"]).await;
1705        cx.assert_shared_state(indoc! {r"func (a string) {
1706            do(something(with<Types>.and_arrays[0, 2]))
1707            ˇ}"})
1708            .await;
1709
1710        // test it works on immediate nesting
1711        cx.set_shared_state("ˇ{()}").await;
1712        cx.simulate_shared_keystrokes(["%"]).await;
1713        cx.assert_shared_state("{()ˇ}").await;
1714        cx.simulate_shared_keystrokes(["%"]).await;
1715        cx.assert_shared_state("ˇ{()}").await;
1716
1717        // test it works on immediate nesting inside braces
1718        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1719        cx.simulate_shared_keystrokes(["%"]).await;
1720        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1721
1722        // test it jumps to the next paren on a line
1723        cx.set_shared_state("func ˇboop() {\n}").await;
1724        cx.simulate_shared_keystrokes(["%"]).await;
1725        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1726    }
1727
1728    #[gpui::test]
1729    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1730        let mut cx = NeovimBackedTestContext::new(cx).await;
1731
1732        // f and F
1733        cx.set_shared_state("ˇone two three four").await;
1734        cx.simulate_shared_keystrokes(["f", "o"]).await;
1735        cx.assert_shared_state("one twˇo three four").await;
1736        cx.simulate_shared_keystrokes([","]).await;
1737        cx.assert_shared_state("ˇone two three four").await;
1738        cx.simulate_shared_keystrokes(["2", ";"]).await;
1739        cx.assert_shared_state("one two three fˇour").await;
1740        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1741        cx.assert_shared_state("one two threˇe four").await;
1742        cx.simulate_shared_keystrokes(["2", ";"]).await;
1743        cx.assert_shared_state("onˇe two three four").await;
1744        cx.simulate_shared_keystrokes([","]).await;
1745        cx.assert_shared_state("one two thrˇee four").await;
1746
1747        // t and T
1748        cx.set_shared_state("ˇone two three four").await;
1749        cx.simulate_shared_keystrokes(["t", "o"]).await;
1750        cx.assert_shared_state("one tˇwo three four").await;
1751        cx.simulate_shared_keystrokes([","]).await;
1752        cx.assert_shared_state("oˇne two three four").await;
1753        cx.simulate_shared_keystrokes(["2", ";"]).await;
1754        cx.assert_shared_state("one two three ˇfour").await;
1755        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1756        cx.assert_shared_state("one two threeˇ four").await;
1757        cx.simulate_shared_keystrokes(["3", ";"]).await;
1758        cx.assert_shared_state("oneˇ two three four").await;
1759        cx.simulate_shared_keystrokes([","]).await;
1760        cx.assert_shared_state("one two thˇree four").await;
1761    }
1762
1763    #[gpui::test]
1764    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1765        let mut cx = NeovimBackedTestContext::new(cx).await;
1766        let initial_state = indoc! {r"something(ˇfoo)"};
1767        cx.set_shared_state(initial_state).await;
1768        cx.simulate_shared_keystrokes(["}"]).await;
1769        cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1770    }
1771
1772    #[gpui::test]
1773    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1774        let mut cx = NeovimBackedTestContext::new(cx).await;
1775        cx.set_shared_state("ˇone\n  two\nthree").await;
1776        cx.simulate_shared_keystrokes(["enter"]).await;
1777        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1778    }
1779
1780    #[gpui::test]
1781    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1782        let mut cx = NeovimBackedTestContext::new(cx).await;
1783        let initial_state = indoc! {r"abc
1784          def
1785          paragraph
1786          the second
1787          third ˇand
1788          final"};
1789
1790        cx.set_shared_state(initial_state).await;
1791        cx.simulate_shared_keystrokes(["shift-h"]).await;
1792        cx.assert_shared_state(indoc! {r"abˇc
1793          def
1794          paragraph
1795          the second
1796          third and
1797          final"})
1798            .await;
1799
1800        // clip point
1801        cx.set_shared_state(indoc! {r"
1802          1 2 3
1803          4 5 6
1804          7 8 ˇ9
1805          "})
1806            .await;
1807        cx.simulate_shared_keystrokes(["shift-h"]).await;
1808        cx.assert_shared_state(indoc! {r"
1809          1 2 ˇ3
1810          4 5 6
1811          7 8 9
1812          "})
1813            .await;
1814
1815        cx.set_shared_state(indoc! {r"
1816          1 2 3
1817          4 5 6
1818          ˇ7 8 9
1819          "})
1820            .await;
1821        cx.simulate_shared_keystrokes(["shift-h"]).await;
1822        cx.assert_shared_state(indoc! {r"
1823          ˇ1 2 3
1824          4 5 6
1825          7 8 9
1826          "})
1827            .await;
1828
1829        cx.set_shared_state(indoc! {r"
1830          1 2 3
1831          4 5 ˇ6
1832          7 8 9"})
1833            .await;
1834        cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1835        cx.assert_shared_state(indoc! {r"
1836          1 2 3
1837          4 5 6
1838          7 8 ˇ9"})
1839            .await;
1840    }
1841
1842    #[gpui::test]
1843    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1844        let mut cx = NeovimBackedTestContext::new(cx).await;
1845        let initial_state = indoc! {r"abˇc
1846          def
1847          paragraph
1848          the second
1849          third and
1850          final"};
1851
1852        cx.set_shared_state(initial_state).await;
1853        cx.simulate_shared_keystrokes(["shift-m"]).await;
1854        cx.assert_shared_state(indoc! {r"abc
1855          def
1856          paˇragraph
1857          the second
1858          third and
1859          final"})
1860            .await;
1861
1862        cx.set_shared_state(indoc! {r"
1863          1 2 3
1864          4 5 6
1865          7 8 ˇ9
1866          "})
1867            .await;
1868        cx.simulate_shared_keystrokes(["shift-m"]).await;
1869        cx.assert_shared_state(indoc! {r"
1870          1 2 3
1871          4 5 ˇ6
1872          7 8 9
1873          "})
1874            .await;
1875        cx.set_shared_state(indoc! {r"
1876          1 2 3
1877          4 5 6
1878          ˇ7 8 9
1879          "})
1880            .await;
1881        cx.simulate_shared_keystrokes(["shift-m"]).await;
1882        cx.assert_shared_state(indoc! {r"
1883          1 2 3
1884          ˇ4 5 6
1885          7 8 9
1886          "})
1887            .await;
1888        cx.set_shared_state(indoc! {r"
1889          ˇ1 2 3
1890          4 5 6
1891          7 8 9
1892          "})
1893            .await;
1894        cx.simulate_shared_keystrokes(["shift-m"]).await;
1895        cx.assert_shared_state(indoc! {r"
1896          1 2 3
1897          ˇ4 5 6
1898          7 8 9
1899          "})
1900            .await;
1901        cx.set_shared_state(indoc! {r"
1902          1 2 3
1903          ˇ4 5 6
1904          7 8 9
1905          "})
1906            .await;
1907        cx.simulate_shared_keystrokes(["shift-m"]).await;
1908        cx.assert_shared_state(indoc! {r"
1909          1 2 3
1910          ˇ4 5 6
1911          7 8 9
1912          "})
1913            .await;
1914        cx.set_shared_state(indoc! {r"
1915          1 2 3
1916          4 5 ˇ6
1917          7 8 9
1918          "})
1919            .await;
1920        cx.simulate_shared_keystrokes(["shift-m"]).await;
1921        cx.assert_shared_state(indoc! {r"
1922          1 2 3
1923          4 5 ˇ6
1924          7 8 9
1925          "})
1926            .await;
1927    }
1928
1929    #[gpui::test]
1930    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1931        let mut cx = NeovimBackedTestContext::new(cx).await;
1932        let initial_state = indoc! {r"abc
1933          deˇf
1934          paragraph
1935          the second
1936          third and
1937          final"};
1938
1939        cx.set_shared_state(initial_state).await;
1940        cx.simulate_shared_keystrokes(["shift-l"]).await;
1941        cx.assert_shared_state(indoc! {r"abc
1942          def
1943          paragraph
1944          the second
1945          third and
1946          fiˇnal"})
1947            .await;
1948
1949        cx.set_shared_state(indoc! {r"
1950          1 2 3
1951          4 5 ˇ6
1952          7 8 9
1953          "})
1954            .await;
1955        cx.simulate_shared_keystrokes(["shift-l"]).await;
1956        cx.assert_shared_state(indoc! {r"
1957          1 2 3
1958          4 5 6
1959          7 8 9
1960          ˇ"})
1961            .await;
1962
1963        cx.set_shared_state(indoc! {r"
1964          1 2 3
1965          ˇ4 5 6
1966          7 8 9
1967          "})
1968            .await;
1969        cx.simulate_shared_keystrokes(["shift-l"]).await;
1970        cx.assert_shared_state(indoc! {r"
1971          1 2 3
1972          4 5 6
1973          7 8 9
1974          ˇ"})
1975            .await;
1976
1977        cx.set_shared_state(indoc! {r"
1978          1 2 ˇ3
1979          4 5 6
1980          7 8 9
1981          "})
1982            .await;
1983        cx.simulate_shared_keystrokes(["shift-l"]).await;
1984        cx.assert_shared_state(indoc! {r"
1985          1 2 3
1986          4 5 6
1987          7 8 9
1988          ˇ"})
1989            .await;
1990
1991        cx.set_shared_state(indoc! {r"
1992          ˇ1 2 3
1993          4 5 6
1994          7 8 9
1995          "})
1996            .await;
1997        cx.simulate_shared_keystrokes(["shift-l"]).await;
1998        cx.assert_shared_state(indoc! {r"
1999          1 2 3
2000          4 5 6
2001          7 8 9
2002          ˇ"})
2003            .await;
2004
2005        cx.set_shared_state(indoc! {r"
2006          1 2 3
2007          4 5 ˇ6
2008          7 8 9
2009          "})
2010            .await;
2011        cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
2012        cx.assert_shared_state(indoc! {r"
2013          1 2 ˇ3
2014          4 5 6
2015          7 8 9
2016          "})
2017            .await;
2018    }
2019
2020    #[gpui::test]
2021    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2022        let mut cx = NeovimBackedTestContext::new(cx).await;
2023        cx.set_shared_state(indoc! {r"
2024        456 5ˇ67 678
2025        "})
2026            .await;
2027        cx.simulate_shared_keystrokes(["g", "e"]).await;
2028        cx.assert_shared_state(indoc! {r"
2029        45ˇ6 567 678
2030        "})
2031            .await;
2032
2033        // Test times
2034        cx.set_shared_state(indoc! {r"
2035        123 234 345
2036        456 5ˇ67 678
2037        "})
2038            .await;
2039        cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2040        cx.assert_shared_state(indoc! {r"
2041        12ˇ3 234 345
2042        456 567 678
2043        "})
2044            .await;
2045
2046        // With punctuation
2047        cx.set_shared_state(indoc! {r"
2048        123 234 345
2049        4;5.6 5ˇ67 678
2050        789 890 901
2051        "})
2052            .await;
2053        cx.simulate_shared_keystrokes(["g", "e"]).await;
2054        cx.assert_shared_state(indoc! {r"
2055          123 234 345
2056          4;5.ˇ6 567 678
2057          789 890 901
2058        "})
2059            .await;
2060
2061        // With punctuation and count
2062        cx.set_shared_state(indoc! {r"
2063        123 234 345
2064        4;5.6 5ˇ67 678
2065        789 890 901
2066        "})
2067            .await;
2068        cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2069        cx.assert_shared_state(indoc! {r"
2070          123 234 345
2071          ˇ4;5.6 567 678
2072          789 890 901
2073        "})
2074            .await;
2075
2076        // newlines
2077        cx.set_shared_state(indoc! {r"
2078        123 234 345
2079
2080        78ˇ9 890 901
2081        "})
2082            .await;
2083        cx.simulate_shared_keystrokes(["g", "e"]).await;
2084        cx.assert_shared_state(indoc! {r"
2085          123 234 345
2086          ˇ
2087          789 890 901
2088        "})
2089            .await;
2090        cx.simulate_shared_keystrokes(["g", "e"]).await;
2091        cx.assert_shared_state(indoc! {r"
2092          123 234 34ˇ5
2093
2094          789 890 901
2095        "})
2096            .await;
2097
2098        // With punctuation
2099        cx.set_shared_state(indoc! {r"
2100        123 234 345
2101        4;5.ˇ6 567 678
2102        789 890 901
2103        "})
2104            .await;
2105        cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2106        cx.assert_shared_state(indoc! {r"
2107          123 234 34ˇ5
2108          4;5.6 567 678
2109          789 890 901
2110        "})
2111            .await;
2112    }
2113
2114    #[gpui::test]
2115    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2116        let mut cx = NeovimBackedTestContext::new(cx).await;
2117
2118        cx.set_shared_state(indoc! {"
2119            fn aˇ() {
2120              return
2121            }
2122        "})
2123            .await;
2124        cx.simulate_shared_keystrokes(["v", "$", "%"]).await;
2125        cx.assert_shared_state(indoc! {"
2126            fn a«() {
2127              return
2128            }ˇ»
2129        "})
2130            .await;
2131    }
2132}