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