motion.rs

   1use editor::{
   2    char_kind,
   3    display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
   4    movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
   5    Bias, CharKind, DisplayPoint, ToOffset,
   6};
   7use gpui::{actions, impl_actions, AppContext, WindowContext};
   8use language::{Point, Selection, SelectionGoal};
   9use serde::Deserialize;
  10use workspace::Workspace;
  11
  12use crate::{
  13    normal::normal_motion,
  14    state::{Mode, Operator},
  15    visual::visual_motion,
  16    Vim,
  17};
  18
  19#[derive(Clone, Debug, PartialEq, Eq)]
  20pub enum Motion {
  21    Left,
  22    Backspace,
  23    Down { display_lines: bool },
  24    Up { display_lines: bool },
  25    Right,
  26    NextWordStart { ignore_punctuation: bool },
  27    NextWordEnd { ignore_punctuation: bool },
  28    PreviousWordStart { ignore_punctuation: bool },
  29    FirstNonWhitespace { display_lines: bool },
  30    CurrentLine,
  31    StartOfLine { display_lines: bool },
  32    EndOfLine { display_lines: bool },
  33    StartOfParagraph,
  34    EndOfParagraph,
  35    StartOfDocument,
  36    EndOfDocument,
  37    Matching,
  38    FindForward { before: bool, char: char },
  39    FindBackward { after: bool, char: char },
  40    NextLineStart,
  41    StartOfLineDownward,
  42    EndOfLineDownward,
  43    GoToColumn,
  44}
  45
  46#[derive(Clone, Deserialize, PartialEq)]
  47#[serde(rename_all = "camelCase")]
  48struct NextWordStart {
  49    #[serde(default)]
  50    ignore_punctuation: bool,
  51}
  52
  53#[derive(Clone, Deserialize, PartialEq)]
  54#[serde(rename_all = "camelCase")]
  55struct NextWordEnd {
  56    #[serde(default)]
  57    ignore_punctuation: bool,
  58}
  59
  60#[derive(Clone, Deserialize, PartialEq)]
  61#[serde(rename_all = "camelCase")]
  62struct PreviousWordStart {
  63    #[serde(default)]
  64    ignore_punctuation: bool,
  65}
  66
  67#[derive(Clone, Deserialize, PartialEq)]
  68#[serde(rename_all = "camelCase")]
  69pub(crate) struct Up {
  70    #[serde(default)]
  71    pub(crate) display_lines: bool,
  72}
  73
  74#[derive(Clone, Deserialize, PartialEq)]
  75#[serde(rename_all = "camelCase")]
  76struct Down {
  77    #[serde(default)]
  78    display_lines: bool,
  79}
  80
  81#[derive(Clone, Deserialize, PartialEq)]
  82#[serde(rename_all = "camelCase")]
  83struct FirstNonWhitespace {
  84    #[serde(default)]
  85    display_lines: bool,
  86}
  87
  88#[derive(Clone, Deserialize, PartialEq)]
  89#[serde(rename_all = "camelCase")]
  90struct EndOfLine {
  91    #[serde(default)]
  92    display_lines: bool,
  93}
  94
  95#[derive(Clone, Deserialize, PartialEq)]
  96#[serde(rename_all = "camelCase")]
  97pub struct StartOfLine {
  98    #[serde(default)]
  99    pub(crate) display_lines: bool,
 100}
 101
 102#[derive(Clone, Deserialize, PartialEq)]
 103struct RepeatFind {
 104    #[serde(default)]
 105    backwards: bool,
 106}
 107
 108actions!(
 109    vim,
 110    [
 111        Left,
 112        Backspace,
 113        Right,
 114        CurrentLine,
 115        StartOfParagraph,
 116        EndOfParagraph,
 117        StartOfDocument,
 118        EndOfDocument,
 119        Matching,
 120        NextLineStart,
 121        StartOfLineDownward,
 122        EndOfLineDownward,
 123        GoToColumn,
 124    ]
 125);
 126impl_actions!(
 127    vim,
 128    [
 129        NextWordStart,
 130        NextWordEnd,
 131        PreviousWordStart,
 132        RepeatFind,
 133        Up,
 134        Down,
 135        FirstNonWhitespace,
 136        EndOfLine,
 137        StartOfLine,
 138    ]
 139);
 140
 141pub fn init(cx: &mut AppContext) {
 142    cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 143    cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
 144    cx.add_action(|_: &mut Workspace, action: &Down, cx: _| {
 145        motion(
 146            Motion::Down {
 147                display_lines: action.display_lines,
 148            },
 149            cx,
 150        )
 151    });
 152    cx.add_action(|_: &mut Workspace, action: &Up, cx: _| {
 153        motion(
 154            Motion::Up {
 155                display_lines: action.display_lines,
 156            },
 157            cx,
 158        )
 159    });
 160    cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 161    cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
 162        motion(
 163            Motion::FirstNonWhitespace {
 164                display_lines: action.display_lines,
 165            },
 166            cx,
 167        )
 168    });
 169    cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
 170        motion(
 171            Motion::StartOfLine {
 172                display_lines: action.display_lines,
 173            },
 174            cx,
 175        )
 176    });
 177    cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
 178        motion(
 179            Motion::EndOfLine {
 180                display_lines: action.display_lines,
 181            },
 182            cx,
 183        )
 184    });
 185    cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
 186    cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
 187        motion(Motion::StartOfParagraph, cx)
 188    });
 189    cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
 190        motion(Motion::EndOfParagraph, cx)
 191    });
 192    cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
 193        motion(Motion::StartOfDocument, cx)
 194    });
 195    cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
 196    cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
 197
 198    cx.add_action(
 199        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
 200            motion(Motion::NextWordStart { ignore_punctuation }, cx)
 201        },
 202    );
 203    cx.add_action(
 204        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
 205            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
 206        },
 207    );
 208    cx.add_action(
 209        |_: &mut Workspace,
 210         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
 211         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
 212    );
 213    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
 214    cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
 215        motion(Motion::StartOfLineDownward, cx)
 216    });
 217    cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
 218        motion(Motion::EndOfLineDownward, cx)
 219    });
 220    cx.add_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
 221    cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
 222        repeat_motion(action.backwards, cx)
 223    })
 224}
 225
 226pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 227    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
 228        Vim::read(cx).active_operator()
 229    {
 230        Vim::update(cx, |vim, cx| vim.pop_operator(cx));
 231    }
 232
 233    let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
 234    let operator = Vim::read(cx).active_operator();
 235    match Vim::read(cx).state().mode {
 236        Mode::Normal => normal_motion(motion, operator, count, cx),
 237        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
 238        Mode::Insert => {
 239            // Shouldn't execute a motion in insert mode. Ignoring
 240        }
 241    }
 242    Vim::update(cx, |vim, cx| vim.clear_operator(cx));
 243}
 244
 245fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
 246    let find = match Vim::read(cx).workspace_state.last_find.clone() {
 247        Some(Motion::FindForward { before, char }) => {
 248            if backwards {
 249                Motion::FindBackward {
 250                    after: before,
 251                    char,
 252                }
 253            } else {
 254                Motion::FindForward { before, char }
 255            }
 256        }
 257
 258        Some(Motion::FindBackward { after, char }) => {
 259            if backwards {
 260                Motion::FindForward {
 261                    before: after,
 262                    char,
 263                }
 264            } else {
 265                Motion::FindBackward { after, char }
 266            }
 267        }
 268        _ => return,
 269    };
 270
 271    motion(find, cx)
 272}
 273
 274// Motion handling is specified here:
 275// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 276impl Motion {
 277    pub fn linewise(&self) -> bool {
 278        use Motion::*;
 279        match self {
 280            Down { .. }
 281            | Up { .. }
 282            | StartOfDocument
 283            | EndOfDocument
 284            | CurrentLine
 285            | NextLineStart
 286            | StartOfLineDownward
 287            | StartOfParagraph
 288            | EndOfParagraph => true,
 289            EndOfLine { .. }
 290            | NextWordEnd { .. }
 291            | Matching
 292            | FindForward { .. }
 293            | Left
 294            | Backspace
 295            | Right
 296            | StartOfLine { .. }
 297            | EndOfLineDownward
 298            | GoToColumn
 299            | NextWordStart { .. }
 300            | PreviousWordStart { .. }
 301            | FirstNonWhitespace { .. }
 302            | FindBackward { .. } => false,
 303        }
 304    }
 305
 306    pub fn infallible(&self) -> bool {
 307        use Motion::*;
 308        match self {
 309            StartOfDocument | EndOfDocument | CurrentLine => true,
 310            Down { .. }
 311            | Up { .. }
 312            | EndOfLine { .. }
 313            | NextWordEnd { .. }
 314            | Matching
 315            | FindForward { .. }
 316            | Left
 317            | Backspace
 318            | Right
 319            | StartOfLine { .. }
 320            | StartOfParagraph
 321            | EndOfParagraph
 322            | StartOfLineDownward
 323            | EndOfLineDownward
 324            | GoToColumn
 325            | NextWordStart { .. }
 326            | PreviousWordStart { .. }
 327            | FirstNonWhitespace { .. }
 328            | FindBackward { .. }
 329            | NextLineStart => false,
 330        }
 331    }
 332
 333    pub fn inclusive(&self) -> bool {
 334        use Motion::*;
 335        match self {
 336            Down { .. }
 337            | Up { .. }
 338            | StartOfDocument
 339            | EndOfDocument
 340            | CurrentLine
 341            | EndOfLine { .. }
 342            | EndOfLineDownward
 343            | NextWordEnd { .. }
 344            | Matching
 345            | FindForward { .. }
 346            | NextLineStart => true,
 347            Left
 348            | Backspace
 349            | Right
 350            | StartOfLine { .. }
 351            | StartOfLineDownward
 352            | StartOfParagraph
 353            | EndOfParagraph
 354            | GoToColumn
 355            | NextWordStart { .. }
 356            | PreviousWordStart { .. }
 357            | FirstNonWhitespace { .. }
 358            | FindBackward { .. } => false,
 359        }
 360    }
 361
 362    pub fn move_point(
 363        &self,
 364        map: &DisplaySnapshot,
 365        point: DisplayPoint,
 366        goal: SelectionGoal,
 367        maybe_times: Option<usize>,
 368        text_layout_details: &TextLayoutDetails,
 369    ) -> Option<(DisplayPoint, SelectionGoal)> {
 370        let times = maybe_times.unwrap_or(1);
 371        use Motion::*;
 372        let infallible = self.infallible();
 373        let (new_point, goal) = match self {
 374            Left => (left(map, point, times), SelectionGoal::None),
 375            Backspace => (backspace(map, point, times), SelectionGoal::None),
 376            Down {
 377                display_lines: false,
 378            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
 379            Down {
 380                display_lines: true,
 381            } => down_display(map, point, goal, times, &text_layout_details),
 382            Up {
 383                display_lines: false,
 384            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
 385            Up {
 386                display_lines: true,
 387            } => up_display(map, point, goal, times, &text_layout_details),
 388            Right => (right(map, point, times), SelectionGoal::None),
 389            NextWordStart { ignore_punctuation } => (
 390                next_word_start(map, point, *ignore_punctuation, times),
 391                SelectionGoal::None,
 392            ),
 393            NextWordEnd { ignore_punctuation } => (
 394                next_word_end(map, point, *ignore_punctuation, times),
 395                SelectionGoal::None,
 396            ),
 397            PreviousWordStart { ignore_punctuation } => (
 398                previous_word_start(map, point, *ignore_punctuation, times),
 399                SelectionGoal::None,
 400            ),
 401            FirstNonWhitespace { display_lines } => (
 402                first_non_whitespace(map, *display_lines, point),
 403                SelectionGoal::None,
 404            ),
 405            StartOfLine { display_lines } => (
 406                start_of_line(map, *display_lines, point),
 407                SelectionGoal::None,
 408            ),
 409            EndOfLine { display_lines } => {
 410                (end_of_line(map, *display_lines, point), SelectionGoal::None)
 411            }
 412            StartOfParagraph => (
 413                movement::start_of_paragraph(map, point, times),
 414                SelectionGoal::None,
 415            ),
 416            EndOfParagraph => (
 417                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 418                SelectionGoal::None,
 419            ),
 420            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 421            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 422            EndOfDocument => (
 423                end_of_document(map, point, maybe_times),
 424                SelectionGoal::None,
 425            ),
 426            Matching => (matching(map, point), SelectionGoal::None),
 427            FindForward { before, char } => (
 428                find_forward(map, point, *before, *char, times),
 429                SelectionGoal::None,
 430            ),
 431            FindBackward { after, char } => (
 432                find_backward(map, point, *after, *char, times),
 433                SelectionGoal::None,
 434            ),
 435            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 436            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 437            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
 438            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
 439        };
 440
 441        (new_point != point || infallible).then_some((new_point, goal))
 442    }
 443
 444    // Expands a selection using self motion for an operator
 445    pub fn expand_selection(
 446        &self,
 447        map: &DisplaySnapshot,
 448        selection: &mut Selection<DisplayPoint>,
 449        times: Option<usize>,
 450        expand_to_surrounding_newline: bool,
 451        text_layout_details: &TextLayoutDetails,
 452    ) -> bool {
 453        if let Some((new_head, goal)) = self.move_point(
 454            map,
 455            selection.head(),
 456            selection.goal,
 457            times,
 458            &text_layout_details,
 459        ) {
 460            selection.set_head(new_head, goal);
 461
 462            if self.linewise() {
 463                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 464
 465                if expand_to_surrounding_newline {
 466                    if selection.end.row() < map.max_point().row() {
 467                        *selection.end.row_mut() += 1;
 468                        *selection.end.column_mut() = 0;
 469                        selection.end = map.clip_point(selection.end, Bias::Right);
 470                        // Don't reset the end here
 471                        return true;
 472                    } else if selection.start.row() > 0 {
 473                        *selection.start.row_mut() -= 1;
 474                        *selection.start.column_mut() = map.line_len(selection.start.row());
 475                        selection.start = map.clip_point(selection.start, Bias::Left);
 476                    }
 477                }
 478
 479                (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
 480            } else {
 481                // Another special case: When using the "w" motion in combination with an
 482                // operator and the last word moved over is at the end of a line, the end of
 483                // that word becomes the end of the operated text, not the first word in the
 484                // next line.
 485                if let Motion::NextWordStart {
 486                    ignore_punctuation: _,
 487                } = self
 488                {
 489                    let start_row = selection.start.to_point(&map).row;
 490                    if selection.end.to_point(&map).row > start_row {
 491                        selection.end =
 492                            Point::new(start_row, map.buffer_snapshot.line_len(start_row))
 493                                .to_display_point(&map)
 494                    }
 495                }
 496
 497                // If the motion is exclusive and the end of the motion is in column 1, the
 498                // end of the motion is moved to the end of the previous line and the motion
 499                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
 500                // but "d}" will not include that line.
 501                let mut inclusive = self.inclusive();
 502                if !inclusive
 503                    && self != &Motion::Backspace
 504                    && selection.end.row() > selection.start.row()
 505                    && selection.end.column() == 0
 506                {
 507                    inclusive = true;
 508                    *selection.end.row_mut() -= 1;
 509                    *selection.end.column_mut() = 0;
 510                    selection.end = map.clip_point(
 511                        map.next_line_boundary(selection.end.to_point(map)).1,
 512                        Bias::Left,
 513                    );
 514                }
 515
 516                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 517                    *selection.end.column_mut() += 1;
 518                }
 519            }
 520            true
 521        } else {
 522            false
 523        }
 524    }
 525}
 526
 527fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 528    for _ in 0..times {
 529        point = movement::saturating_left(map, point);
 530        if point.column() == 0 {
 531            break;
 532        }
 533    }
 534    point
 535}
 536
 537fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 538    for _ in 0..times {
 539        point = movement::left(map, point);
 540    }
 541    point
 542}
 543
 544pub(crate) fn start_of_relative_buffer_row(
 545    map: &DisplaySnapshot,
 546    point: DisplayPoint,
 547    times: isize,
 548) -> DisplayPoint {
 549    let start = map.display_point_to_fold_point(point, Bias::Left);
 550    let target = start.row() as isize + times;
 551    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 552
 553    map.clip_point(
 554        map.fold_point_to_display_point(
 555            map.fold_snapshot
 556                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 557        ),
 558        Bias::Right,
 559    )
 560}
 561
 562fn up_down_buffer_rows(
 563    map: &DisplaySnapshot,
 564    point: DisplayPoint,
 565    mut goal: SelectionGoal,
 566    times: isize,
 567    text_layout_details: &TextLayoutDetails,
 568) -> (DisplayPoint, SelectionGoal) {
 569    let start = map.display_point_to_fold_point(point, Bias::Left);
 570    let begin_folded_line = map.fold_point_to_display_point(
 571        map.fold_snapshot
 572            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
 573    );
 574    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 575
 576    let (goal_wrap, goal_x) = match goal {
 577        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
 578        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 579        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 580        _ => {
 581            let x = map.x_for_point(point, text_layout_details);
 582            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
 583            (select_nth_wrapped_row, x)
 584        }
 585    };
 586
 587    let target = start.row() as isize + times;
 588    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 589
 590    let mut begin_folded_line = map.fold_point_to_display_point(
 591        map.fold_snapshot
 592            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
 593    );
 594
 595    let mut i = 0;
 596    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
 597        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
 598        if map
 599            .display_point_to_fold_point(next_folded_line, Bias::Right)
 600            .row()
 601            == new_row
 602        {
 603            i += 1;
 604            begin_folded_line = next_folded_line;
 605        } else {
 606            break;
 607        }
 608    }
 609
 610    let new_col = if i == goal_wrap {
 611        map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
 612    } else {
 613        map.line_len(begin_folded_line.row())
 614    };
 615
 616    (
 617        map.clip_point(
 618            DisplayPoint::new(begin_folded_line.row(), new_col),
 619            Bias::Left,
 620        ),
 621        goal,
 622    )
 623}
 624
 625fn down_display(
 626    map: &DisplaySnapshot,
 627    mut point: DisplayPoint,
 628    mut goal: SelectionGoal,
 629    times: usize,
 630    text_layout_details: &TextLayoutDetails,
 631) -> (DisplayPoint, SelectionGoal) {
 632    for _ in 0..times {
 633        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
 634    }
 635
 636    (point, goal)
 637}
 638
 639fn up_display(
 640    map: &DisplaySnapshot,
 641    mut point: DisplayPoint,
 642    mut goal: SelectionGoal,
 643    times: usize,
 644    text_layout_details: &TextLayoutDetails,
 645) -> (DisplayPoint, SelectionGoal) {
 646    for _ in 0..times {
 647        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
 648    }
 649
 650    (point, goal)
 651}
 652
 653pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 654    for _ in 0..times {
 655        let new_point = movement::saturating_right(map, point);
 656        if point == new_point {
 657            break;
 658        }
 659        point = new_point;
 660    }
 661    point
 662}
 663
 664pub(crate) fn next_word_start(
 665    map: &DisplaySnapshot,
 666    mut point: DisplayPoint,
 667    ignore_punctuation: bool,
 668    times: usize,
 669) -> DisplayPoint {
 670    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 671    for _ in 0..times {
 672        let mut crossed_newline = false;
 673        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 674            let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 675            let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 676            let at_newline = right == '\n';
 677
 678            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
 679                || at_newline && crossed_newline
 680                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 681
 682            crossed_newline |= at_newline;
 683            found
 684        })
 685    }
 686    point
 687}
 688
 689fn next_word_end(
 690    map: &DisplaySnapshot,
 691    mut point: DisplayPoint,
 692    ignore_punctuation: bool,
 693    times: usize,
 694) -> DisplayPoint {
 695    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 696    for _ in 0..times {
 697        if point.column() < map.line_len(point.row()) {
 698            *point.column_mut() += 1;
 699        } else if point.row() < map.max_buffer_row() {
 700            *point.row_mut() += 1;
 701            *point.column_mut() = 0;
 702        }
 703        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 704            let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 705            let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 706
 707            left_kind != right_kind && left_kind != CharKind::Whitespace
 708        });
 709
 710        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
 711        // we have backtracked already
 712        if !map
 713            .chars_at(point)
 714            .nth(1)
 715            .map(|(c, _)| c == '\n')
 716            .unwrap_or(true)
 717        {
 718            *point.column_mut() = point.column().saturating_sub(1);
 719        }
 720        point = map.clip_point(point, Bias::Left);
 721    }
 722    point
 723}
 724
 725fn previous_word_start(
 726    map: &DisplaySnapshot,
 727    mut point: DisplayPoint,
 728    ignore_punctuation: bool,
 729    times: usize,
 730) -> DisplayPoint {
 731    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 732    for _ in 0..times {
 733        // This works even though find_preceding_boundary is called for every character in the line containing
 734        // cursor because the newline is checked only once.
 735        point =
 736            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
 737                let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 738                let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 739
 740                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
 741            });
 742    }
 743    point
 744}
 745
 746pub(crate) fn first_non_whitespace(
 747    map: &DisplaySnapshot,
 748    display_lines: bool,
 749    from: DisplayPoint,
 750) -> DisplayPoint {
 751    let mut last_point = start_of_line(map, display_lines, from);
 752    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
 753    for (ch, point) in map.chars_at(last_point) {
 754        if ch == '\n' {
 755            return from;
 756        }
 757
 758        last_point = point;
 759
 760        if char_kind(&scope, ch) != CharKind::Whitespace {
 761            break;
 762        }
 763    }
 764
 765    map.clip_point(last_point, Bias::Left)
 766}
 767
 768pub(crate) fn start_of_line(
 769    map: &DisplaySnapshot,
 770    display_lines: bool,
 771    point: DisplayPoint,
 772) -> DisplayPoint {
 773    if display_lines {
 774        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
 775    } else {
 776        map.prev_line_boundary(point.to_point(map)).1
 777    }
 778}
 779
 780pub(crate) fn end_of_line(
 781    map: &DisplaySnapshot,
 782    display_lines: bool,
 783    point: DisplayPoint,
 784) -> DisplayPoint {
 785    if display_lines {
 786        map.clip_point(
 787            DisplayPoint::new(point.row(), map.line_len(point.row())),
 788            Bias::Left,
 789        )
 790    } else {
 791        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
 792    }
 793}
 794
 795fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
 796    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
 797    *new_point.column_mut() = point.column();
 798    map.clip_point(new_point, Bias::Left)
 799}
 800
 801fn end_of_document(
 802    map: &DisplaySnapshot,
 803    point: DisplayPoint,
 804    line: Option<usize>,
 805) -> DisplayPoint {
 806    let new_row = if let Some(line) = line {
 807        (line - 1) as u32
 808    } else {
 809        map.max_buffer_row()
 810    };
 811
 812    let new_point = Point::new(new_row, point.column());
 813    map.clip_point(new_point.to_display_point(map), Bias::Left)
 814}
 815
 816fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 817    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
 818    let point = display_point.to_point(map);
 819    let offset = point.to_offset(&map.buffer_snapshot);
 820
 821    // Ensure the range is contained by the current line.
 822    let mut line_end = map.next_line_boundary(point).0;
 823    if line_end == point {
 824        line_end = map.max_point().to_point(map);
 825    }
 826
 827    let line_range = map.prev_line_boundary(point).0..line_end;
 828    let visible_line_range =
 829        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
 830    let ranges = map
 831        .buffer_snapshot
 832        .bracket_ranges(visible_line_range.clone());
 833    if let Some(ranges) = ranges {
 834        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
 835            ..line_range.end.to_offset(&map.buffer_snapshot);
 836        let mut closest_pair_destination = None;
 837        let mut closest_distance = usize::MAX;
 838
 839        for (open_range, close_range) in ranges {
 840            if open_range.start >= offset && line_range.contains(&open_range.start) {
 841                let distance = open_range.start - offset;
 842                if distance < closest_distance {
 843                    closest_pair_destination = Some(close_range.start);
 844                    closest_distance = distance;
 845                    continue;
 846                }
 847            }
 848
 849            if close_range.start >= offset && line_range.contains(&close_range.start) {
 850                let distance = close_range.start - offset;
 851                if distance < closest_distance {
 852                    closest_pair_destination = Some(open_range.start);
 853                    closest_distance = distance;
 854                    continue;
 855                }
 856            }
 857
 858            continue;
 859        }
 860
 861        closest_pair_destination
 862            .map(|destination| destination.to_display_point(map))
 863            .unwrap_or(display_point)
 864    } else {
 865        display_point
 866    }
 867}
 868
 869fn find_forward(
 870    map: &DisplaySnapshot,
 871    from: DisplayPoint,
 872    before: bool,
 873    target: char,
 874    times: usize,
 875) -> DisplayPoint {
 876    let mut to = from;
 877    let mut found = false;
 878
 879    for _ in 0..times {
 880        found = false;
 881        to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
 882            found = right == target;
 883            found
 884        });
 885    }
 886
 887    if found {
 888        if before && to.column() > 0 {
 889            *to.column_mut() -= 1;
 890            map.clip_point(to, Bias::Left)
 891        } else {
 892            to
 893        }
 894    } else {
 895        from
 896    }
 897}
 898
 899fn find_backward(
 900    map: &DisplaySnapshot,
 901    from: DisplayPoint,
 902    after: bool,
 903    target: char,
 904    times: usize,
 905) -> DisplayPoint {
 906    let mut to = from;
 907
 908    for _ in 0..times {
 909        to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
 910    }
 911
 912    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
 913        if after {
 914            *to.column_mut() += 1;
 915            map.clip_point(to, Bias::Right)
 916        } else {
 917            to
 918        }
 919    } else {
 920        from
 921    }
 922}
 923
 924fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
 925    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
 926    first_non_whitespace(map, false, correct_line)
 927}
 928
 929fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
 930    let correct_line = start_of_relative_buffer_row(map, point, 0);
 931    right(map, correct_line, times.saturating_sub(1))
 932}
 933
 934pub(crate) fn next_line_end(
 935    map: &DisplaySnapshot,
 936    mut point: DisplayPoint,
 937    times: usize,
 938) -> DisplayPoint {
 939    if times > 1 {
 940        point = start_of_relative_buffer_row(map, point, times as isize - 1);
 941    }
 942    end_of_line(map, false, point)
 943}
 944
 945#[cfg(test)]
 946
 947mod test {
 948
 949    use crate::test::NeovimBackedTestContext;
 950    use indoc::indoc;
 951
 952    #[gpui::test]
 953    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
 954        let mut cx = NeovimBackedTestContext::new(cx).await;
 955
 956        let initial_state = indoc! {r"ˇabc
 957            def
 958
 959            paragraph
 960            the second
 961
 962
 963
 964            third and
 965            final"};
 966
 967        // goes down once
 968        cx.set_shared_state(initial_state).await;
 969        cx.simulate_shared_keystrokes(["}"]).await;
 970        cx.assert_shared_state(indoc! {r"abc
 971            def
 972            ˇ
 973            paragraph
 974            the second
 975
 976
 977
 978            third and
 979            final"})
 980            .await;
 981
 982        // goes up once
 983        cx.simulate_shared_keystrokes(["{"]).await;
 984        cx.assert_shared_state(initial_state).await;
 985
 986        // goes down twice
 987        cx.simulate_shared_keystrokes(["2", "}"]).await;
 988        cx.assert_shared_state(indoc! {r"abc
 989            def
 990
 991            paragraph
 992            the second
 993            ˇ
 994
 995
 996            third and
 997            final"})
 998            .await;
 999
1000        // goes down over multiple blanks
1001        cx.simulate_shared_keystrokes(["}"]).await;
1002        cx.assert_shared_state(indoc! {r"abc
1003                def
1004
1005                paragraph
1006                the second
1007
1008
1009
1010                third and
1011                finaˇl"})
1012            .await;
1013
1014        // goes up twice
1015        cx.simulate_shared_keystrokes(["2", "{"]).await;
1016        cx.assert_shared_state(indoc! {r"abc
1017                def
1018                ˇ
1019                paragraph
1020                the second
1021
1022
1023
1024                third and
1025                final"})
1026            .await
1027    }
1028
1029    #[gpui::test]
1030    async fn test_matching(cx: &mut gpui::TestAppContext) {
1031        let mut cx = NeovimBackedTestContext::new(cx).await;
1032
1033        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1034                do(something(with<Types>.and_arrays[0, 2]))
1035            }"})
1036            .await;
1037        cx.simulate_shared_keystrokes(["%"]).await;
1038        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1039                do(something(with<Types>.and_arrays[0, 2]))
1040            }"})
1041            .await;
1042
1043        // test it works on the last character of the line
1044        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1045            do(something(with<Types>.and_arrays[0, 2]))
1046            }"})
1047            .await;
1048        cx.simulate_shared_keystrokes(["%"]).await;
1049        cx.assert_shared_state(indoc! {r"func (a string) {
1050            do(something(with<Types>.and_arrays[0, 2]))
1051            ˇ}"})
1052            .await;
1053
1054        // test it works on immediate nesting
1055        cx.set_shared_state("ˇ{()}").await;
1056        cx.simulate_shared_keystrokes(["%"]).await;
1057        cx.assert_shared_state("{()ˇ}").await;
1058        cx.simulate_shared_keystrokes(["%"]).await;
1059        cx.assert_shared_state("ˇ{()}").await;
1060
1061        // test it works on immediate nesting inside braces
1062        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1063        cx.simulate_shared_keystrokes(["%"]).await;
1064        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1065
1066        // test it jumps to the next paren on a line
1067        cx.set_shared_state("func ˇboop() {\n}").await;
1068        cx.simulate_shared_keystrokes(["%"]).await;
1069        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1070    }
1071
1072    #[gpui::test]
1073    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1074        let mut cx = NeovimBackedTestContext::new(cx).await;
1075
1076        cx.set_shared_state("ˇone two three four").await;
1077        cx.simulate_shared_keystrokes(["f", "o"]).await;
1078        cx.assert_shared_state("one twˇo three four").await;
1079        cx.simulate_shared_keystrokes([","]).await;
1080        cx.assert_shared_state("ˇone two three four").await;
1081        cx.simulate_shared_keystrokes(["2", ";"]).await;
1082        cx.assert_shared_state("one two three fˇour").await;
1083        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1084        cx.assert_shared_state("one two threeˇ four").await;
1085        cx.simulate_shared_keystrokes(["3", ";"]).await;
1086        cx.assert_shared_state("oneˇ two three four").await;
1087        cx.simulate_shared_keystrokes([","]).await;
1088        cx.assert_shared_state("one two thˇree four").await;
1089    }
1090
1091    #[gpui::test]
1092    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1093        let mut cx = NeovimBackedTestContext::new(cx).await;
1094        cx.set_shared_state("ˇone\n  two\nthree").await;
1095        cx.simulate_shared_keystrokes(["enter"]).await;
1096        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1097    }
1098}