motion.rs

   1use editor::{
   2    display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
   3    movement::{
   4        self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
   5    },
   6    scroll::Autoscroll,
   7    Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
   8};
   9use gpui::{action_with_deprecated_aliases, actions, impl_actions, px, Context, Window};
  10use language::{CharKind, Point, Selection, SelectionGoal};
  11use multi_buffer::MultiBufferRow;
  12use schemars::JsonSchema;
  13use serde::Deserialize;
  14use std::ops::Range;
  15use workspace::searchable::Direction;
  16
  17use crate::{
  18    normal::mark,
  19    state::{Mode, Operator},
  20    surrounds::SurroundsType,
  21    Vim,
  22};
  23
  24#[derive(Clone, Debug, PartialEq, Eq)]
  25pub enum Motion {
  26    Left,
  27    WrappingLeft,
  28    Down {
  29        display_lines: bool,
  30    },
  31    Up {
  32        display_lines: bool,
  33    },
  34    Right,
  35    WrappingRight,
  36    NextWordStart {
  37        ignore_punctuation: bool,
  38    },
  39    NextWordEnd {
  40        ignore_punctuation: bool,
  41    },
  42    PreviousWordStart {
  43        ignore_punctuation: bool,
  44    },
  45    PreviousWordEnd {
  46        ignore_punctuation: bool,
  47    },
  48    NextSubwordStart {
  49        ignore_punctuation: bool,
  50    },
  51    NextSubwordEnd {
  52        ignore_punctuation: bool,
  53    },
  54    PreviousSubwordStart {
  55        ignore_punctuation: bool,
  56    },
  57    PreviousSubwordEnd {
  58        ignore_punctuation: bool,
  59    },
  60    FirstNonWhitespace {
  61        display_lines: bool,
  62    },
  63    CurrentLine,
  64    StartOfLine {
  65        display_lines: bool,
  66    },
  67    EndOfLine {
  68        display_lines: bool,
  69    },
  70    SentenceBackward,
  71    SentenceForward,
  72    StartOfParagraph,
  73    EndOfParagraph,
  74    StartOfDocument,
  75    EndOfDocument,
  76    Matching,
  77    UnmatchedForward {
  78        char: char,
  79    },
  80    UnmatchedBackward {
  81        char: char,
  82    },
  83    FindForward {
  84        before: bool,
  85        char: char,
  86        mode: FindRange,
  87        smartcase: bool,
  88    },
  89    FindBackward {
  90        after: bool,
  91        char: char,
  92        mode: FindRange,
  93        smartcase: bool,
  94    },
  95    Sneak {
  96        first_char: char,
  97        second_char: char,
  98        smartcase: bool,
  99    },
 100    SneakBackward {
 101        first_char: char,
 102        second_char: char,
 103        smartcase: bool,
 104    },
 105    RepeatFind {
 106        last_find: Box<Motion>,
 107    },
 108    RepeatFindReversed {
 109        last_find: Box<Motion>,
 110    },
 111    NextLineStart,
 112    PreviousLineStart,
 113    StartOfLineDownward,
 114    EndOfLineDownward,
 115    GoToColumn,
 116    WindowTop,
 117    WindowMiddle,
 118    WindowBottom,
 119    NextSectionStart,
 120    NextSectionEnd,
 121    PreviousSectionStart,
 122    PreviousSectionEnd,
 123    NextMethodStart,
 124    NextMethodEnd,
 125    PreviousMethodStart,
 126    PreviousMethodEnd,
 127    NextComment,
 128    PreviousComment,
 129
 130    // we don't have a good way to run a search synchronously, so
 131    // we handle search motions by running the search async and then
 132    // calling back into motion with this
 133    ZedSearchResult {
 134        prior_selections: Vec<Range<Anchor>>,
 135        new_selections: Vec<Range<Anchor>>,
 136    },
 137    Jump {
 138        anchor: Anchor,
 139        line: bool,
 140    },
 141}
 142
 143#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 144#[serde(deny_unknown_fields)]
 145struct NextWordStart {
 146    #[serde(default)]
 147    ignore_punctuation: bool,
 148}
 149
 150#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 151#[serde(deny_unknown_fields)]
 152struct NextWordEnd {
 153    #[serde(default)]
 154    ignore_punctuation: bool,
 155}
 156
 157#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 158#[serde(deny_unknown_fields)]
 159struct PreviousWordStart {
 160    #[serde(default)]
 161    ignore_punctuation: bool,
 162}
 163
 164#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 165#[serde(deny_unknown_fields)]
 166struct PreviousWordEnd {
 167    #[serde(default)]
 168    ignore_punctuation: bool,
 169}
 170
 171#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 172#[serde(deny_unknown_fields)]
 173pub(crate) struct NextSubwordStart {
 174    #[serde(default)]
 175    pub(crate) ignore_punctuation: bool,
 176}
 177
 178#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 179#[serde(deny_unknown_fields)]
 180pub(crate) struct NextSubwordEnd {
 181    #[serde(default)]
 182    pub(crate) ignore_punctuation: bool,
 183}
 184
 185#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 186#[serde(deny_unknown_fields)]
 187pub(crate) struct PreviousSubwordStart {
 188    #[serde(default)]
 189    pub(crate) ignore_punctuation: bool,
 190}
 191
 192#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 193#[serde(deny_unknown_fields)]
 194pub(crate) struct PreviousSubwordEnd {
 195    #[serde(default)]
 196    pub(crate) ignore_punctuation: bool,
 197}
 198
 199#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 200#[serde(deny_unknown_fields)]
 201pub(crate) struct Up {
 202    #[serde(default)]
 203    pub(crate) display_lines: bool,
 204}
 205
 206#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 207#[serde(deny_unknown_fields)]
 208pub(crate) struct Down {
 209    #[serde(default)]
 210    pub(crate) display_lines: bool,
 211}
 212
 213#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 214#[serde(deny_unknown_fields)]
 215struct FirstNonWhitespace {
 216    #[serde(default)]
 217    display_lines: bool,
 218}
 219
 220#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 221#[serde(deny_unknown_fields)]
 222struct EndOfLine {
 223    #[serde(default)]
 224    display_lines: bool,
 225}
 226
 227#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 228#[serde(deny_unknown_fields)]
 229pub struct StartOfLine {
 230    #[serde(default)]
 231    pub(crate) display_lines: bool,
 232}
 233
 234#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 235#[serde(deny_unknown_fields)]
 236struct UnmatchedForward {
 237    #[serde(default)]
 238    char: char,
 239}
 240
 241#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 242#[serde(deny_unknown_fields)]
 243struct UnmatchedBackward {
 244    #[serde(default)]
 245    char: char,
 246}
 247
 248impl_actions!(
 249    vim,
 250    [
 251        StartOfLine,
 252        EndOfLine,
 253        FirstNonWhitespace,
 254        Down,
 255        Up,
 256        NextWordStart,
 257        NextWordEnd,
 258        PreviousWordStart,
 259        PreviousWordEnd,
 260        NextSubwordStart,
 261        NextSubwordEnd,
 262        PreviousSubwordStart,
 263        PreviousSubwordEnd,
 264        UnmatchedForward,
 265        UnmatchedBackward
 266    ]
 267);
 268
 269actions!(
 270    vim,
 271    [
 272        Left,
 273        Backspace,
 274        Right,
 275        Space,
 276        CurrentLine,
 277        SentenceForward,
 278        SentenceBackward,
 279        StartOfParagraph,
 280        EndOfParagraph,
 281        StartOfDocument,
 282        EndOfDocument,
 283        Matching,
 284        NextLineStart,
 285        PreviousLineStart,
 286        StartOfLineDownward,
 287        EndOfLineDownward,
 288        GoToColumn,
 289        RepeatFind,
 290        RepeatFindReversed,
 291        WindowTop,
 292        WindowMiddle,
 293        WindowBottom,
 294        NextSectionStart,
 295        NextSectionEnd,
 296        PreviousSectionStart,
 297        PreviousSectionEnd,
 298        NextMethodStart,
 299        NextMethodEnd,
 300        PreviousMethodStart,
 301        PreviousMethodEnd,
 302        NextComment,
 303        PreviousComment,
 304    ]
 305);
 306
 307action_with_deprecated_aliases!(vim, WrappingLeft, ["vim::Backspace"]);
 308action_with_deprecated_aliases!(vim, WrappingRight, ["vim::Space"]);
 309
 310pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 311    Vim::action(editor, cx, |vim, _: &Left, window, cx| {
 312        vim.motion(Motion::Left, window, cx)
 313    });
 314    Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
 315        vim.motion(Motion::WrappingLeft, window, cx)
 316    });
 317    // Deprecated.
 318    Vim::action(editor, cx, |vim, _: &Backspace, window, cx| {
 319        vim.motion(Motion::WrappingLeft, window, cx)
 320    });
 321    Vim::action(editor, cx, |vim, action: &Down, window, cx| {
 322        vim.motion(
 323            Motion::Down {
 324                display_lines: action.display_lines,
 325            },
 326            window,
 327            cx,
 328        )
 329    });
 330    Vim::action(editor, cx, |vim, action: &Up, window, cx| {
 331        vim.motion(
 332            Motion::Up {
 333                display_lines: action.display_lines,
 334            },
 335            window,
 336            cx,
 337        )
 338    });
 339    Vim::action(editor, cx, |vim, _: &Right, window, cx| {
 340        vim.motion(Motion::Right, window, cx)
 341    });
 342    Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
 343        vim.motion(Motion::WrappingRight, window, cx)
 344    });
 345    // Deprecated.
 346    Vim::action(editor, cx, |vim, _: &Space, window, cx| {
 347        vim.motion(Motion::WrappingRight, window, cx)
 348    });
 349    Vim::action(
 350        editor,
 351        cx,
 352        |vim, action: &FirstNonWhitespace, window, cx| {
 353            vim.motion(
 354                Motion::FirstNonWhitespace {
 355                    display_lines: action.display_lines,
 356                },
 357                window,
 358                cx,
 359            )
 360        },
 361    );
 362    Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
 363        vim.motion(
 364            Motion::StartOfLine {
 365                display_lines: action.display_lines,
 366            },
 367            window,
 368            cx,
 369        )
 370    });
 371    Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
 372        vim.motion(
 373            Motion::EndOfLine {
 374                display_lines: action.display_lines,
 375            },
 376            window,
 377            cx,
 378        )
 379    });
 380    Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
 381        vim.motion(Motion::CurrentLine, window, cx)
 382    });
 383    Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
 384        vim.motion(Motion::StartOfParagraph, window, cx)
 385    });
 386    Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
 387        vim.motion(Motion::EndOfParagraph, window, cx)
 388    });
 389
 390    Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
 391        vim.motion(Motion::SentenceForward, window, cx)
 392    });
 393    Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
 394        vim.motion(Motion::SentenceBackward, window, cx)
 395    });
 396    Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
 397        vim.motion(Motion::StartOfDocument, window, cx)
 398    });
 399    Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
 400        vim.motion(Motion::EndOfDocument, window, cx)
 401    });
 402    Vim::action(editor, cx, |vim, _: &Matching, window, cx| {
 403        vim.motion(Motion::Matching, window, cx)
 404    });
 405    Vim::action(
 406        editor,
 407        cx,
 408        |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
 409            vim.motion(Motion::UnmatchedForward { char }, window, cx)
 410        },
 411    );
 412    Vim::action(
 413        editor,
 414        cx,
 415        |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
 416            vim.motion(Motion::UnmatchedBackward { char }, window, cx)
 417        },
 418    );
 419    Vim::action(
 420        editor,
 421        cx,
 422        |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
 423            vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
 424        },
 425    );
 426    Vim::action(
 427        editor,
 428        cx,
 429        |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
 430            vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
 431        },
 432    );
 433    Vim::action(
 434        editor,
 435        cx,
 436        |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
 437            vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
 438        },
 439    );
 440    Vim::action(
 441        editor,
 442        cx,
 443        |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
 444            vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
 445        },
 446    );
 447    Vim::action(
 448        editor,
 449        cx,
 450        |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
 451            vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
 452        },
 453    );
 454    Vim::action(
 455        editor,
 456        cx,
 457        |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
 458            vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
 459        },
 460    );
 461    Vim::action(
 462        editor,
 463        cx,
 464        |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
 465            vim.motion(
 466                Motion::PreviousSubwordStart { ignore_punctuation },
 467                window,
 468                cx,
 469            )
 470        },
 471    );
 472    Vim::action(
 473        editor,
 474        cx,
 475        |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
 476            vim.motion(
 477                Motion::PreviousSubwordEnd { ignore_punctuation },
 478                window,
 479                cx,
 480            )
 481        },
 482    );
 483    Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
 484        vim.motion(Motion::NextLineStart, window, cx)
 485    });
 486    Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
 487        vim.motion(Motion::PreviousLineStart, window, cx)
 488    });
 489    Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
 490        vim.motion(Motion::StartOfLineDownward, window, cx)
 491    });
 492    Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
 493        vim.motion(Motion::EndOfLineDownward, window, cx)
 494    });
 495    Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
 496        vim.motion(Motion::GoToColumn, window, cx)
 497    });
 498
 499    Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
 500        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 501            vim.motion(Motion::RepeatFind { last_find }, window, cx);
 502        }
 503    });
 504
 505    Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
 506        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 507            vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
 508        }
 509    });
 510    Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
 511        vim.motion(Motion::WindowTop, window, cx)
 512    });
 513    Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
 514        vim.motion(Motion::WindowMiddle, window, cx)
 515    });
 516    Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
 517        vim.motion(Motion::WindowBottom, window, cx)
 518    });
 519
 520    Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
 521        vim.motion(Motion::PreviousSectionStart, window, cx)
 522    });
 523    Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
 524        vim.motion(Motion::NextSectionStart, window, cx)
 525    });
 526    Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
 527        vim.motion(Motion::PreviousSectionEnd, window, cx)
 528    });
 529    Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
 530        vim.motion(Motion::NextSectionEnd, window, cx)
 531    });
 532    Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
 533        vim.motion(Motion::PreviousMethodStart, window, cx)
 534    });
 535    Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
 536        vim.motion(Motion::NextMethodStart, window, cx)
 537    });
 538    Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
 539        vim.motion(Motion::PreviousMethodEnd, window, cx)
 540    });
 541    Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
 542        vim.motion(Motion::NextMethodEnd, window, cx)
 543    });
 544    Vim::action(editor, cx, |vim, &NextComment, window, cx| {
 545        vim.motion(Motion::NextComment, window, cx)
 546    });
 547    Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
 548        vim.motion(Motion::PreviousComment, window, cx)
 549    });
 550}
 551
 552impl Vim {
 553    pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
 554        if let Motion::ZedSearchResult {
 555            prior_selections, ..
 556        } = &m
 557        {
 558            match self.mode {
 559                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 560                    if !prior_selections.is_empty() {
 561                        self.update_editor(window, cx, |_, editor, window, cx| {
 562                            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 563                                s.select_ranges(prior_selections.iter().cloned())
 564                            })
 565                        });
 566                    }
 567                }
 568                Mode::Normal | Mode::Replace | Mode::Insert => {
 569                    if self.active_operator().is_none() {
 570                        return;
 571                    }
 572                }
 573
 574                Mode::HelixNormal => {}
 575            }
 576        }
 577
 578        self.motion(m, window, cx)
 579    }
 580
 581    pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context<Self>) {
 582        if let Some(Operator::FindForward { .. })
 583        | Some(Operator::Sneak { .. })
 584        | Some(Operator::SneakBackward { .. })
 585        | Some(Operator::FindBackward { .. }) = self.active_operator()
 586        {
 587            self.pop_operator(window, cx);
 588        }
 589
 590        let count = Vim::take_count(cx);
 591        let active_operator = self.active_operator();
 592        let mut waiting_operator: Option<Operator> = None;
 593        match self.mode {
 594            Mode::Normal | Mode::Replace | Mode::Insert => {
 595                if active_operator == Some(Operator::AddSurrounds { target: None }) {
 596                    waiting_operator = Some(Operator::AddSurrounds {
 597                        target: Some(SurroundsType::Motion(motion)),
 598                    });
 599                } else {
 600                    self.normal_motion(motion.clone(), active_operator.clone(), count, window, cx)
 601                }
 602            }
 603            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 604                self.visual_motion(motion.clone(), count, window, cx)
 605            }
 606
 607            Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, window, cx),
 608        }
 609        self.clear_operator(window, cx);
 610        if let Some(operator) = waiting_operator {
 611            self.push_operator(operator, window, cx);
 612            Vim::globals(cx).pre_count = count
 613        }
 614    }
 615}
 616
 617// Motion handling is specified here:
 618// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 619impl Motion {
 620    pub fn linewise(&self) -> bool {
 621        use Motion::*;
 622        match self {
 623            Down { .. }
 624            | Up { .. }
 625            | StartOfDocument
 626            | EndOfDocument
 627            | CurrentLine
 628            | NextLineStart
 629            | PreviousLineStart
 630            | StartOfLineDownward
 631            | StartOfParagraph
 632            | EndOfParagraph
 633            | WindowTop
 634            | WindowMiddle
 635            | WindowBottom
 636            | NextSectionStart
 637            | NextSectionEnd
 638            | PreviousSectionStart
 639            | PreviousSectionEnd
 640            | NextMethodStart
 641            | NextMethodEnd
 642            | PreviousMethodStart
 643            | PreviousMethodEnd
 644            | NextComment
 645            | PreviousComment
 646            | Jump { line: true, .. } => true,
 647            EndOfLine { .. }
 648            | Matching
 649            | UnmatchedForward { .. }
 650            | UnmatchedBackward { .. }
 651            | FindForward { .. }
 652            | Left
 653            | WrappingLeft
 654            | Right
 655            | SentenceBackward
 656            | SentenceForward
 657            | WrappingRight
 658            | StartOfLine { .. }
 659            | EndOfLineDownward
 660            | GoToColumn
 661            | NextWordStart { .. }
 662            | NextWordEnd { .. }
 663            | PreviousWordStart { .. }
 664            | PreviousWordEnd { .. }
 665            | NextSubwordStart { .. }
 666            | NextSubwordEnd { .. }
 667            | PreviousSubwordStart { .. }
 668            | PreviousSubwordEnd { .. }
 669            | FirstNonWhitespace { .. }
 670            | FindBackward { .. }
 671            | Sneak { .. }
 672            | SneakBackward { .. }
 673            | RepeatFind { .. }
 674            | RepeatFindReversed { .. }
 675            | Jump { line: false, .. }
 676            | ZedSearchResult { .. } => false,
 677        }
 678    }
 679
 680    pub fn infallible(&self) -> bool {
 681        use Motion::*;
 682        match self {
 683            StartOfDocument | EndOfDocument | CurrentLine => true,
 684            Down { .. }
 685            | Up { .. }
 686            | EndOfLine { .. }
 687            | Matching
 688            | UnmatchedForward { .. }
 689            | UnmatchedBackward { .. }
 690            | FindForward { .. }
 691            | RepeatFind { .. }
 692            | Left
 693            | WrappingLeft
 694            | Right
 695            | WrappingRight
 696            | StartOfLine { .. }
 697            | StartOfParagraph
 698            | EndOfParagraph
 699            | SentenceBackward
 700            | SentenceForward
 701            | StartOfLineDownward
 702            | EndOfLineDownward
 703            | GoToColumn
 704            | NextWordStart { .. }
 705            | NextWordEnd { .. }
 706            | PreviousWordStart { .. }
 707            | PreviousWordEnd { .. }
 708            | NextSubwordStart { .. }
 709            | NextSubwordEnd { .. }
 710            | PreviousSubwordStart { .. }
 711            | PreviousSubwordEnd { .. }
 712            | FirstNonWhitespace { .. }
 713            | FindBackward { .. }
 714            | Sneak { .. }
 715            | SneakBackward { .. }
 716            | RepeatFindReversed { .. }
 717            | WindowTop
 718            | WindowMiddle
 719            | WindowBottom
 720            | NextLineStart
 721            | PreviousLineStart
 722            | ZedSearchResult { .. }
 723            | NextSectionStart
 724            | NextSectionEnd
 725            | PreviousSectionStart
 726            | PreviousSectionEnd
 727            | NextMethodStart
 728            | NextMethodEnd
 729            | PreviousMethodStart
 730            | PreviousMethodEnd
 731            | NextComment
 732            | PreviousComment
 733            | Jump { .. } => false,
 734        }
 735    }
 736
 737    pub fn inclusive(&self) -> bool {
 738        use Motion::*;
 739        match self {
 740            Down { .. }
 741            | Up { .. }
 742            | StartOfDocument
 743            | EndOfDocument
 744            | CurrentLine
 745            | EndOfLine { .. }
 746            | EndOfLineDownward
 747            | Matching
 748            | UnmatchedForward { .. }
 749            | UnmatchedBackward { .. }
 750            | FindForward { .. }
 751            | WindowTop
 752            | WindowMiddle
 753            | WindowBottom
 754            | NextWordEnd { .. }
 755            | PreviousWordEnd { .. }
 756            | NextSubwordEnd { .. }
 757            | PreviousSubwordEnd { .. }
 758            | NextLineStart
 759            | PreviousLineStart => true,
 760            Left
 761            | WrappingLeft
 762            | Right
 763            | WrappingRight
 764            | StartOfLine { .. }
 765            | StartOfLineDownward
 766            | StartOfParagraph
 767            | EndOfParagraph
 768            | SentenceBackward
 769            | SentenceForward
 770            | GoToColumn
 771            | NextWordStart { .. }
 772            | PreviousWordStart { .. }
 773            | NextSubwordStart { .. }
 774            | PreviousSubwordStart { .. }
 775            | FirstNonWhitespace { .. }
 776            | FindBackward { .. }
 777            | Sneak { .. }
 778            | SneakBackward { .. }
 779            | Jump { .. }
 780            | NextSectionStart
 781            | NextSectionEnd
 782            | PreviousSectionStart
 783            | PreviousSectionEnd
 784            | NextMethodStart
 785            | NextMethodEnd
 786            | PreviousMethodStart
 787            | PreviousMethodEnd
 788            | NextComment
 789            | PreviousComment
 790            | ZedSearchResult { .. } => false,
 791            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 792                motion.inclusive()
 793            }
 794        }
 795    }
 796
 797    pub fn move_point(
 798        &self,
 799        map: &DisplaySnapshot,
 800        point: DisplayPoint,
 801        goal: SelectionGoal,
 802        maybe_times: Option<usize>,
 803        text_layout_details: &TextLayoutDetails,
 804    ) -> Option<(DisplayPoint, SelectionGoal)> {
 805        let times = maybe_times.unwrap_or(1);
 806        use Motion::*;
 807        let infallible = self.infallible();
 808        let (new_point, goal) = match self {
 809            Left => (left(map, point, times), SelectionGoal::None),
 810            WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None),
 811            Down {
 812                display_lines: false,
 813            } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
 814            Down {
 815                display_lines: true,
 816            } => down_display(map, point, goal, times, text_layout_details),
 817            Up {
 818                display_lines: false,
 819            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
 820            Up {
 821                display_lines: true,
 822            } => up_display(map, point, goal, times, text_layout_details),
 823            Right => (right(map, point, times), SelectionGoal::None),
 824            WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None),
 825            NextWordStart { ignore_punctuation } => (
 826                next_word_start(map, point, *ignore_punctuation, times),
 827                SelectionGoal::None,
 828            ),
 829            NextWordEnd { ignore_punctuation } => (
 830                next_word_end(map, point, *ignore_punctuation, times, true),
 831                SelectionGoal::None,
 832            ),
 833            PreviousWordStart { ignore_punctuation } => (
 834                previous_word_start(map, point, *ignore_punctuation, times),
 835                SelectionGoal::None,
 836            ),
 837            PreviousWordEnd { ignore_punctuation } => (
 838                previous_word_end(map, point, *ignore_punctuation, times),
 839                SelectionGoal::None,
 840            ),
 841            NextSubwordStart { ignore_punctuation } => (
 842                next_subword_start(map, point, *ignore_punctuation, times),
 843                SelectionGoal::None,
 844            ),
 845            NextSubwordEnd { ignore_punctuation } => (
 846                next_subword_end(map, point, *ignore_punctuation, times, true),
 847                SelectionGoal::None,
 848            ),
 849            PreviousSubwordStart { ignore_punctuation } => (
 850                previous_subword_start(map, point, *ignore_punctuation, times),
 851                SelectionGoal::None,
 852            ),
 853            PreviousSubwordEnd { ignore_punctuation } => (
 854                previous_subword_end(map, point, *ignore_punctuation, times),
 855                SelectionGoal::None,
 856            ),
 857            FirstNonWhitespace { display_lines } => (
 858                first_non_whitespace(map, *display_lines, point),
 859                SelectionGoal::None,
 860            ),
 861            StartOfLine { display_lines } => (
 862                start_of_line(map, *display_lines, point),
 863                SelectionGoal::None,
 864            ),
 865            EndOfLine { display_lines } => (
 866                end_of_line(map, *display_lines, point, times),
 867                SelectionGoal::None,
 868            ),
 869            SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
 870            SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
 871            StartOfParagraph => (
 872                movement::start_of_paragraph(map, point, times),
 873                SelectionGoal::None,
 874            ),
 875            EndOfParagraph => (
 876                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 877                SelectionGoal::None,
 878            ),
 879            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 880            StartOfDocument => (
 881                start_of_document(map, point, maybe_times),
 882                SelectionGoal::None,
 883            ),
 884            EndOfDocument => (
 885                end_of_document(map, point, maybe_times),
 886                SelectionGoal::None,
 887            ),
 888            Matching => (matching(map, point), SelectionGoal::None),
 889            UnmatchedForward { char } => (
 890                unmatched_forward(map, point, *char, times),
 891                SelectionGoal::None,
 892            ),
 893            UnmatchedBackward { char } => (
 894                unmatched_backward(map, point, *char, times),
 895                SelectionGoal::None,
 896            ),
 897            // t f
 898            FindForward {
 899                before,
 900                char,
 901                mode,
 902                smartcase,
 903            } => {
 904                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
 905                    .map(|new_point| (new_point, SelectionGoal::None))
 906            }
 907            // T F
 908            FindBackward {
 909                after,
 910                char,
 911                mode,
 912                smartcase,
 913            } => (
 914                find_backward(map, point, *after, *char, times, *mode, *smartcase),
 915                SelectionGoal::None,
 916            ),
 917            Sneak {
 918                first_char,
 919                second_char,
 920                smartcase,
 921            } => {
 922                return sneak(map, point, *first_char, *second_char, times, *smartcase)
 923                    .map(|new_point| (new_point, SelectionGoal::None));
 924            }
 925            SneakBackward {
 926                first_char,
 927                second_char,
 928                smartcase,
 929            } => {
 930                return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
 931                    .map(|new_point| (new_point, SelectionGoal::None));
 932            }
 933            // ; -- repeat the last find done with t, f, T, F
 934            RepeatFind { last_find } => match **last_find {
 935                Motion::FindForward {
 936                    before,
 937                    char,
 938                    mode,
 939                    smartcase,
 940                } => {
 941                    let mut new_point =
 942                        find_forward(map, point, before, char, times, mode, smartcase);
 943                    if new_point == Some(point) {
 944                        new_point =
 945                            find_forward(map, point, before, char, times + 1, mode, smartcase);
 946                    }
 947
 948                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 949                }
 950
 951                Motion::FindBackward {
 952                    after,
 953                    char,
 954                    mode,
 955                    smartcase,
 956                } => {
 957                    let mut new_point =
 958                        find_backward(map, point, after, char, times, mode, smartcase);
 959                    if new_point == point {
 960                        new_point =
 961                            find_backward(map, point, after, char, times + 1, mode, smartcase);
 962                    }
 963
 964                    (new_point, SelectionGoal::None)
 965                }
 966                Motion::Sneak {
 967                    first_char,
 968                    second_char,
 969                    smartcase,
 970                } => {
 971                    let mut new_point =
 972                        sneak(map, point, first_char, second_char, times, smartcase);
 973                    if new_point == Some(point) {
 974                        new_point =
 975                            sneak(map, point, first_char, second_char, times + 1, smartcase);
 976                    }
 977
 978                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 979                }
 980
 981                Motion::SneakBackward {
 982                    first_char,
 983                    second_char,
 984                    smartcase,
 985                } => {
 986                    let mut new_point =
 987                        sneak_backward(map, point, first_char, second_char, times, smartcase);
 988                    if new_point == Some(point) {
 989                        new_point = sneak_backward(
 990                            map,
 991                            point,
 992                            first_char,
 993                            second_char,
 994                            times + 1,
 995                            smartcase,
 996                        );
 997                    }
 998
 999                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1000                }
1001                _ => return None,
1002            },
1003            // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
1004            RepeatFindReversed { last_find } => match **last_find {
1005                Motion::FindForward {
1006                    before,
1007                    char,
1008                    mode,
1009                    smartcase,
1010                } => {
1011                    let mut new_point =
1012                        find_backward(map, point, before, char, times, mode, smartcase);
1013                    if new_point == point {
1014                        new_point =
1015                            find_backward(map, point, before, char, times + 1, mode, smartcase);
1016                    }
1017
1018                    (new_point, SelectionGoal::None)
1019                }
1020
1021                Motion::FindBackward {
1022                    after,
1023                    char,
1024                    mode,
1025                    smartcase,
1026                } => {
1027                    let mut new_point =
1028                        find_forward(map, point, after, char, times, mode, smartcase);
1029                    if new_point == Some(point) {
1030                        new_point =
1031                            find_forward(map, point, after, char, times + 1, mode, smartcase);
1032                    }
1033
1034                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1035                }
1036
1037                Motion::Sneak {
1038                    first_char,
1039                    second_char,
1040                    smartcase,
1041                } => {
1042                    let mut new_point =
1043                        sneak_backward(map, point, first_char, second_char, times, smartcase);
1044                    if new_point == Some(point) {
1045                        new_point = sneak_backward(
1046                            map,
1047                            point,
1048                            first_char,
1049                            second_char,
1050                            times + 1,
1051                            smartcase,
1052                        );
1053                    }
1054
1055                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1056                }
1057
1058                Motion::SneakBackward {
1059                    first_char,
1060                    second_char,
1061                    smartcase,
1062                } => {
1063                    let mut new_point =
1064                        sneak(map, point, first_char, second_char, times, smartcase);
1065                    if new_point == Some(point) {
1066                        new_point =
1067                            sneak(map, point, first_char, second_char, times + 1, smartcase);
1068                    }
1069
1070                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1071                }
1072                _ => return None,
1073            },
1074            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
1075            PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
1076            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
1077            EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
1078            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
1079            WindowTop => window_top(map, point, text_layout_details, times - 1),
1080            WindowMiddle => window_middle(map, point, text_layout_details),
1081            WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
1082            Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
1083            ZedSearchResult { new_selections, .. } => {
1084                // There will be only one selection, as
1085                // Search::SelectNextMatch selects a single match.
1086                if let Some(new_selection) = new_selections.first() {
1087                    (
1088                        new_selection.start.to_display_point(map),
1089                        SelectionGoal::None,
1090                    )
1091                } else {
1092                    return None;
1093                }
1094            }
1095            NextSectionStart => (
1096                section_motion(map, point, times, Direction::Next, true),
1097                SelectionGoal::None,
1098            ),
1099            NextSectionEnd => (
1100                section_motion(map, point, times, Direction::Next, false),
1101                SelectionGoal::None,
1102            ),
1103            PreviousSectionStart => (
1104                section_motion(map, point, times, Direction::Prev, true),
1105                SelectionGoal::None,
1106            ),
1107            PreviousSectionEnd => (
1108                section_motion(map, point, times, Direction::Prev, false),
1109                SelectionGoal::None,
1110            ),
1111
1112            NextMethodStart => (
1113                method_motion(map, point, times, Direction::Next, true),
1114                SelectionGoal::None,
1115            ),
1116            NextMethodEnd => (
1117                method_motion(map, point, times, Direction::Next, false),
1118                SelectionGoal::None,
1119            ),
1120            PreviousMethodStart => (
1121                method_motion(map, point, times, Direction::Prev, true),
1122                SelectionGoal::None,
1123            ),
1124            PreviousMethodEnd => (
1125                method_motion(map, point, times, Direction::Prev, false),
1126                SelectionGoal::None,
1127            ),
1128            NextComment => (
1129                comment_motion(map, point, times, Direction::Next),
1130                SelectionGoal::None,
1131            ),
1132            PreviousComment => (
1133                comment_motion(map, point, times, Direction::Prev),
1134                SelectionGoal::None,
1135            ),
1136        };
1137
1138        (new_point != point || infallible).then_some((new_point, goal))
1139    }
1140
1141    // Get the range value after self is applied to the specified selection.
1142    pub fn range(
1143        &self,
1144        map: &DisplaySnapshot,
1145        selection: Selection<DisplayPoint>,
1146        times: Option<usize>,
1147        expand_to_surrounding_newline: bool,
1148        text_layout_details: &TextLayoutDetails,
1149    ) -> Option<Range<DisplayPoint>> {
1150        if let Motion::ZedSearchResult {
1151            prior_selections,
1152            new_selections,
1153        } = self
1154        {
1155            if let Some((prior_selection, new_selection)) =
1156                prior_selections.first().zip(new_selections.first())
1157            {
1158                let start = prior_selection
1159                    .start
1160                    .to_display_point(map)
1161                    .min(new_selection.start.to_display_point(map));
1162                let end = new_selection
1163                    .end
1164                    .to_display_point(map)
1165                    .max(prior_selection.end.to_display_point(map));
1166
1167                if start < end {
1168                    return Some(start..end);
1169                } else {
1170                    return Some(end..start);
1171                }
1172            } else {
1173                return None;
1174            }
1175        }
1176
1177        if let Some((new_head, goal)) = self.move_point(
1178            map,
1179            selection.head(),
1180            selection.goal,
1181            times,
1182            text_layout_details,
1183        ) {
1184            let mut selection = selection.clone();
1185            selection.set_head(new_head, goal);
1186
1187            if self.linewise() {
1188                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1189
1190                if expand_to_surrounding_newline {
1191                    if selection.end.row() < map.max_point().row() {
1192                        *selection.end.row_mut() += 1;
1193                        *selection.end.column_mut() = 0;
1194                        selection.end = map.clip_point(selection.end, Bias::Right);
1195                        // Don't reset the end here
1196                        return Some(selection.start..selection.end);
1197                    } else if selection.start.row().0 > 0 {
1198                        *selection.start.row_mut() -= 1;
1199                        *selection.start.column_mut() = map.line_len(selection.start.row());
1200                        selection.start = map.clip_point(selection.start, Bias::Left);
1201                    }
1202                }
1203
1204                selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1205            } else {
1206                // Another special case: When using the "w" motion in combination with an
1207                // operator and the last word moved over is at the end of a line, the end of
1208                // that word becomes the end of the operated text, not the first word in the
1209                // next line.
1210                if let Motion::NextWordStart {
1211                    ignore_punctuation: _,
1212                } = self
1213                {
1214                    let start_row = MultiBufferRow(selection.start.to_point(map).row);
1215                    if selection.end.to_point(map).row > start_row.0 {
1216                        selection.end =
1217                            Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
1218                                .to_display_point(map)
1219                    }
1220                }
1221
1222                // If the motion is exclusive and the end of the motion is in column 1, the
1223                // end of the motion is moved to the end of the previous line and the motion
1224                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1225                // but "d}" will not include that line.
1226                let mut inclusive = self.inclusive();
1227                let start_point = selection.start.to_point(map);
1228                let mut end_point = selection.end.to_point(map);
1229
1230                // DisplayPoint
1231
1232                if !inclusive
1233                    && self != &Motion::WrappingLeft
1234                    && end_point.row > start_point.row
1235                    && end_point.column == 0
1236                {
1237                    inclusive = true;
1238                    end_point.row -= 1;
1239                    end_point.column = 0;
1240                    selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1241                }
1242
1243                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
1244                    selection.end = movement::saturating_right(map, selection.end)
1245                }
1246            }
1247            Some(selection.start..selection.end)
1248        } else {
1249            None
1250        }
1251    }
1252
1253    // Expands a selection using self for an operator
1254    pub fn expand_selection(
1255        &self,
1256        map: &DisplaySnapshot,
1257        selection: &mut Selection<DisplayPoint>,
1258        times: Option<usize>,
1259        expand_to_surrounding_newline: bool,
1260        text_layout_details: &TextLayoutDetails,
1261    ) -> bool {
1262        if let Some(range) = self.range(
1263            map,
1264            selection.clone(),
1265            times,
1266            expand_to_surrounding_newline,
1267            text_layout_details,
1268        ) {
1269            selection.start = range.start;
1270            selection.end = range.end;
1271            true
1272        } else {
1273            false
1274        }
1275    }
1276}
1277
1278fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1279    for _ in 0..times {
1280        point = movement::saturating_left(map, point);
1281        if point.column() == 0 {
1282            break;
1283        }
1284    }
1285    point
1286}
1287
1288pub(crate) fn wrapping_left(
1289    map: &DisplaySnapshot,
1290    mut point: DisplayPoint,
1291    times: usize,
1292) -> DisplayPoint {
1293    for _ in 0..times {
1294        point = movement::left(map, point);
1295        if point.is_zero() {
1296            break;
1297        }
1298    }
1299    point
1300}
1301
1302fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1303    for _ in 0..times {
1304        point = wrapping_right_single(map, point);
1305        if point == map.max_point() {
1306            break;
1307        }
1308    }
1309    point
1310}
1311
1312fn wrapping_right_single(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
1313    let max_column = map.line_len(point.row()).saturating_sub(1);
1314    if point.column() < max_column {
1315        *point.column_mut() += 1;
1316    } else if point.row() < map.max_point().row() {
1317        *point.row_mut() += 1;
1318        *point.column_mut() = 0;
1319    }
1320    point
1321}
1322
1323pub(crate) fn start_of_relative_buffer_row(
1324    map: &DisplaySnapshot,
1325    point: DisplayPoint,
1326    times: isize,
1327) -> DisplayPoint {
1328    let start = map.display_point_to_fold_point(point, Bias::Left);
1329    let target = start.row() as isize + times;
1330    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1331
1332    map.clip_point(
1333        map.fold_point_to_display_point(
1334            map.fold_snapshot
1335                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1336        ),
1337        Bias::Right,
1338    )
1339}
1340
1341fn up_down_buffer_rows(
1342    map: &DisplaySnapshot,
1343    mut point: DisplayPoint,
1344    mut goal: SelectionGoal,
1345    mut times: isize,
1346    text_layout_details: &TextLayoutDetails,
1347) -> (DisplayPoint, SelectionGoal) {
1348    let bias = if times < 0 { Bias::Left } else { Bias::Right };
1349
1350    while map.is_folded_buffer_header(point.row()) {
1351        if times < 0 {
1352            (point, _) = movement::up(map, point, goal, true, text_layout_details);
1353            times += 1;
1354        } else if times > 0 {
1355            (point, _) = movement::down(map, point, goal, true, text_layout_details);
1356            times -= 1;
1357        } else {
1358            break;
1359        }
1360    }
1361
1362    let start = map.display_point_to_fold_point(point, Bias::Left);
1363    let begin_folded_line = map.fold_point_to_display_point(
1364        map.fold_snapshot
1365            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1366    );
1367    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1368
1369    let (goal_wrap, goal_x) = match goal {
1370        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1371        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1372        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1373        _ => {
1374            let x = map.x_for_display_point(point, text_layout_details);
1375            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1376            (select_nth_wrapped_row, x.0)
1377        }
1378    };
1379
1380    let target = start.row() as isize + times;
1381    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1382
1383    let mut begin_folded_line = map.fold_point_to_display_point(
1384        map.fold_snapshot
1385            .clip_point(FoldPoint::new(new_row, 0), bias),
1386    );
1387
1388    let mut i = 0;
1389    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1390        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1391        if map
1392            .display_point_to_fold_point(next_folded_line, bias)
1393            .row()
1394            == new_row
1395        {
1396            i += 1;
1397            begin_folded_line = next_folded_line;
1398        } else {
1399            break;
1400        }
1401    }
1402
1403    let new_col = if i == goal_wrap {
1404        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1405    } else {
1406        map.line_len(begin_folded_line.row())
1407    };
1408
1409    (
1410        map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
1411        goal,
1412    )
1413}
1414
1415fn down_display(
1416    map: &DisplaySnapshot,
1417    mut point: DisplayPoint,
1418    mut goal: SelectionGoal,
1419    times: usize,
1420    text_layout_details: &TextLayoutDetails,
1421) -> (DisplayPoint, SelectionGoal) {
1422    for _ in 0..times {
1423        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1424    }
1425
1426    (point, goal)
1427}
1428
1429fn up_display(
1430    map: &DisplaySnapshot,
1431    mut point: DisplayPoint,
1432    mut goal: SelectionGoal,
1433    times: usize,
1434    text_layout_details: &TextLayoutDetails,
1435) -> (DisplayPoint, SelectionGoal) {
1436    for _ in 0..times {
1437        (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1438    }
1439
1440    (point, goal)
1441}
1442
1443pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1444    for _ in 0..times {
1445        let new_point = movement::saturating_right(map, point);
1446        if point == new_point {
1447            break;
1448        }
1449        point = new_point;
1450    }
1451    point
1452}
1453
1454pub(crate) fn next_char(
1455    map: &DisplaySnapshot,
1456    point: DisplayPoint,
1457    allow_cross_newline: bool,
1458) -> DisplayPoint {
1459    let mut new_point = point;
1460    let mut max_column = map.line_len(new_point.row());
1461    if !allow_cross_newline {
1462        max_column -= 1;
1463    }
1464    if new_point.column() < max_column {
1465        *new_point.column_mut() += 1;
1466    } else if new_point < map.max_point() && allow_cross_newline {
1467        *new_point.row_mut() += 1;
1468        *new_point.column_mut() = 0;
1469    }
1470    map.clip_ignoring_line_ends(new_point, Bias::Right)
1471}
1472
1473pub(crate) fn next_word_start(
1474    map: &DisplaySnapshot,
1475    mut point: DisplayPoint,
1476    ignore_punctuation: bool,
1477    times: usize,
1478) -> DisplayPoint {
1479    let classifier = map
1480        .buffer_snapshot
1481        .char_classifier_at(point.to_point(map))
1482        .ignore_punctuation(ignore_punctuation);
1483    for _ in 0..times {
1484        let mut crossed_newline = false;
1485        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1486            let left_kind = classifier.kind(left);
1487            let right_kind = classifier.kind(right);
1488            let at_newline = right == '\n';
1489
1490            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1491                || at_newline && crossed_newline
1492                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1493
1494            crossed_newline |= at_newline;
1495            found
1496        });
1497        if point == new_point {
1498            break;
1499        }
1500        point = new_point;
1501    }
1502    point
1503}
1504
1505pub(crate) fn next_word_end(
1506    map: &DisplaySnapshot,
1507    mut point: DisplayPoint,
1508    ignore_punctuation: bool,
1509    times: usize,
1510    allow_cross_newline: bool,
1511) -> DisplayPoint {
1512    let classifier = map
1513        .buffer_snapshot
1514        .char_classifier_at(point.to_point(map))
1515        .ignore_punctuation(ignore_punctuation);
1516    for _ in 0..times {
1517        let new_point = next_char(map, point, allow_cross_newline);
1518        let mut need_next_char = false;
1519        let new_point = movement::find_boundary_exclusive(
1520            map,
1521            new_point,
1522            FindRange::MultiLine,
1523            |left, right| {
1524                let left_kind = classifier.kind(left);
1525                let right_kind = classifier.kind(right);
1526                let at_newline = right == '\n';
1527
1528                if !allow_cross_newline && at_newline {
1529                    need_next_char = true;
1530                    return true;
1531                }
1532
1533                left_kind != right_kind && left_kind != CharKind::Whitespace
1534            },
1535        );
1536        let new_point = if need_next_char {
1537            next_char(map, new_point, true)
1538        } else {
1539            new_point
1540        };
1541        let new_point = map.clip_point(new_point, Bias::Left);
1542        if point == new_point {
1543            break;
1544        }
1545        point = new_point;
1546    }
1547    point
1548}
1549
1550fn previous_word_start(
1551    map: &DisplaySnapshot,
1552    mut point: DisplayPoint,
1553    ignore_punctuation: bool,
1554    times: usize,
1555) -> DisplayPoint {
1556    let classifier = map
1557        .buffer_snapshot
1558        .char_classifier_at(point.to_point(map))
1559        .ignore_punctuation(ignore_punctuation);
1560    for _ in 0..times {
1561        // This works even though find_preceding_boundary is called for every character in the line containing
1562        // cursor because the newline is checked only once.
1563        let new_point = movement::find_preceding_boundary_display_point(
1564            map,
1565            point,
1566            FindRange::MultiLine,
1567            |left, right| {
1568                let left_kind = classifier.kind(left);
1569                let right_kind = classifier.kind(right);
1570
1571                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1572            },
1573        );
1574        if point == new_point {
1575            break;
1576        }
1577        point = new_point;
1578    }
1579    point
1580}
1581
1582fn previous_word_end(
1583    map: &DisplaySnapshot,
1584    point: DisplayPoint,
1585    ignore_punctuation: bool,
1586    times: usize,
1587) -> DisplayPoint {
1588    let classifier = map
1589        .buffer_snapshot
1590        .char_classifier_at(point.to_point(map))
1591        .ignore_punctuation(ignore_punctuation);
1592    let mut point = point.to_point(map);
1593
1594    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1595        point.column += 1;
1596    }
1597    for _ in 0..times {
1598        let new_point = movement::find_preceding_boundary_point(
1599            &map.buffer_snapshot,
1600            point,
1601            FindRange::MultiLine,
1602            |left, right| {
1603                let left_kind = classifier.kind(left);
1604                let right_kind = classifier.kind(right);
1605                match (left_kind, right_kind) {
1606                    (CharKind::Punctuation, CharKind::Whitespace)
1607                    | (CharKind::Punctuation, CharKind::Word)
1608                    | (CharKind::Word, CharKind::Whitespace)
1609                    | (CharKind::Word, CharKind::Punctuation) => true,
1610                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1611                    _ => false,
1612                }
1613            },
1614        );
1615        if new_point == point {
1616            break;
1617        }
1618        point = new_point;
1619    }
1620    movement::saturating_left(map, point.to_display_point(map))
1621}
1622
1623fn next_subword_start(
1624    map: &DisplaySnapshot,
1625    mut point: DisplayPoint,
1626    ignore_punctuation: bool,
1627    times: usize,
1628) -> DisplayPoint {
1629    let classifier = map
1630        .buffer_snapshot
1631        .char_classifier_at(point.to_point(map))
1632        .ignore_punctuation(ignore_punctuation);
1633    for _ in 0..times {
1634        let mut crossed_newline = false;
1635        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1636            let left_kind = classifier.kind(left);
1637            let right_kind = classifier.kind(right);
1638            let at_newline = right == '\n';
1639
1640            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1641            let is_subword_start =
1642                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1643
1644            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1645                || at_newline && crossed_newline
1646                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1647
1648            crossed_newline |= at_newline;
1649            found
1650        });
1651        if point == new_point {
1652            break;
1653        }
1654        point = new_point;
1655    }
1656    point
1657}
1658
1659pub(crate) fn next_subword_end(
1660    map: &DisplaySnapshot,
1661    mut point: DisplayPoint,
1662    ignore_punctuation: bool,
1663    times: usize,
1664    allow_cross_newline: bool,
1665) -> DisplayPoint {
1666    let classifier = map
1667        .buffer_snapshot
1668        .char_classifier_at(point.to_point(map))
1669        .ignore_punctuation(ignore_punctuation);
1670    for _ in 0..times {
1671        let new_point = next_char(map, point, allow_cross_newline);
1672
1673        let mut crossed_newline = false;
1674        let mut need_backtrack = false;
1675        let new_point =
1676            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1677                let left_kind = classifier.kind(left);
1678                let right_kind = classifier.kind(right);
1679                let at_newline = right == '\n';
1680
1681                if !allow_cross_newline && at_newline {
1682                    return true;
1683                }
1684
1685                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1686                let is_subword_end =
1687                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1688
1689                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1690
1691                if found && (is_word_end || is_subword_end) {
1692                    need_backtrack = true;
1693                }
1694
1695                crossed_newline |= at_newline;
1696                found
1697            });
1698        let mut new_point = map.clip_point(new_point, Bias::Left);
1699        if need_backtrack {
1700            *new_point.column_mut() -= 1;
1701        }
1702        let new_point = map.clip_point(new_point, Bias::Left);
1703        if point == new_point {
1704            break;
1705        }
1706        point = new_point;
1707    }
1708    point
1709}
1710
1711fn previous_subword_start(
1712    map: &DisplaySnapshot,
1713    mut point: DisplayPoint,
1714    ignore_punctuation: bool,
1715    times: usize,
1716) -> DisplayPoint {
1717    let classifier = map
1718        .buffer_snapshot
1719        .char_classifier_at(point.to_point(map))
1720        .ignore_punctuation(ignore_punctuation);
1721    for _ in 0..times {
1722        let mut crossed_newline = false;
1723        // This works even though find_preceding_boundary is called for every character in the line containing
1724        // cursor because the newline is checked only once.
1725        let new_point = movement::find_preceding_boundary_display_point(
1726            map,
1727            point,
1728            FindRange::MultiLine,
1729            |left, right| {
1730                let left_kind = classifier.kind(left);
1731                let right_kind = classifier.kind(right);
1732                let at_newline = right == '\n';
1733
1734                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1735                let is_subword_start =
1736                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1737
1738                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1739                    || at_newline && crossed_newline
1740                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1741
1742                crossed_newline |= at_newline;
1743
1744                found
1745            },
1746        );
1747        if point == new_point {
1748            break;
1749        }
1750        point = new_point;
1751    }
1752    point
1753}
1754
1755fn previous_subword_end(
1756    map: &DisplaySnapshot,
1757    point: DisplayPoint,
1758    ignore_punctuation: bool,
1759    times: usize,
1760) -> DisplayPoint {
1761    let classifier = map
1762        .buffer_snapshot
1763        .char_classifier_at(point.to_point(map))
1764        .ignore_punctuation(ignore_punctuation);
1765    let mut point = point.to_point(map);
1766
1767    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1768        point.column += 1;
1769    }
1770    for _ in 0..times {
1771        let new_point = movement::find_preceding_boundary_point(
1772            &map.buffer_snapshot,
1773            point,
1774            FindRange::MultiLine,
1775            |left, right| {
1776                let left_kind = classifier.kind(left);
1777                let right_kind = classifier.kind(right);
1778
1779                let is_subword_end =
1780                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1781
1782                if is_subword_end {
1783                    return true;
1784                }
1785
1786                match (left_kind, right_kind) {
1787                    (CharKind::Word, CharKind::Whitespace)
1788                    | (CharKind::Word, CharKind::Punctuation) => true,
1789                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1790                    _ => false,
1791                }
1792            },
1793        );
1794        if new_point == point {
1795            break;
1796        }
1797        point = new_point;
1798    }
1799    movement::saturating_left(map, point.to_display_point(map))
1800}
1801
1802pub(crate) fn first_non_whitespace(
1803    map: &DisplaySnapshot,
1804    display_lines: bool,
1805    from: DisplayPoint,
1806) -> DisplayPoint {
1807    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1808    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1809    for (ch, offset) in map.buffer_chars_at(start_offset) {
1810        if ch == '\n' {
1811            return from;
1812        }
1813
1814        start_offset = offset;
1815
1816        if classifier.kind(ch) != CharKind::Whitespace {
1817            break;
1818        }
1819    }
1820
1821    start_offset.to_display_point(map)
1822}
1823
1824pub(crate) fn last_non_whitespace(
1825    map: &DisplaySnapshot,
1826    from: DisplayPoint,
1827    count: usize,
1828) -> DisplayPoint {
1829    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1830    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1831
1832    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1833    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1834        if classifier.kind(ch) != CharKind::Whitespace {
1835            return end_of_line.to_display_point(map);
1836        }
1837    }
1838
1839    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1840        if ch == '\n' {
1841            break;
1842        }
1843        end_of_line = offset;
1844        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1845            break;
1846        }
1847    }
1848
1849    end_of_line.to_display_point(map)
1850}
1851
1852pub(crate) fn start_of_line(
1853    map: &DisplaySnapshot,
1854    display_lines: bool,
1855    point: DisplayPoint,
1856) -> DisplayPoint {
1857    if display_lines {
1858        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1859    } else {
1860        map.prev_line_boundary(point.to_point(map)).1
1861    }
1862}
1863
1864pub(crate) fn end_of_line(
1865    map: &DisplaySnapshot,
1866    display_lines: bool,
1867    mut point: DisplayPoint,
1868    times: usize,
1869) -> DisplayPoint {
1870    if times > 1 {
1871        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1872    }
1873    if display_lines {
1874        map.clip_point(
1875            DisplayPoint::new(point.row(), map.line_len(point.row())),
1876            Bias::Left,
1877        )
1878    } else {
1879        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1880    }
1881}
1882
1883fn sentence_backwards(
1884    map: &DisplaySnapshot,
1885    point: DisplayPoint,
1886    mut times: usize,
1887) -> DisplayPoint {
1888    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1889    let mut chars = map.reverse_buffer_chars_at(start).peekable();
1890
1891    let mut was_newline = map
1892        .buffer_chars_at(start)
1893        .next()
1894        .is_some_and(|(c, _)| c == '\n');
1895
1896    while let Some((ch, offset)) = chars.next() {
1897        let start_of_next_sentence = if was_newline && ch == '\n' {
1898            Some(offset + ch.len_utf8())
1899        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1900            Some(next_non_blank(map, offset + ch.len_utf8()))
1901        } else if ch == '.' || ch == '?' || ch == '!' {
1902            start_of_next_sentence(map, offset + ch.len_utf8())
1903        } else {
1904            None
1905        };
1906
1907        if let Some(start_of_next_sentence) = start_of_next_sentence {
1908            if start_of_next_sentence < start {
1909                times = times.saturating_sub(1);
1910            }
1911            if times == 0 || offset == 0 {
1912                return map.clip_point(
1913                    start_of_next_sentence
1914                        .to_offset(&map.buffer_snapshot)
1915                        .to_display_point(map),
1916                    Bias::Left,
1917                );
1918            }
1919        }
1920        if was_newline {
1921            start = offset;
1922        }
1923        was_newline = ch == '\n';
1924    }
1925
1926    DisplayPoint::zero()
1927}
1928
1929fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1930    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1931    let mut chars = map.buffer_chars_at(start).peekable();
1932
1933    let mut was_newline = map
1934        .reverse_buffer_chars_at(start)
1935        .next()
1936        .is_some_and(|(c, _)| c == '\n')
1937        && chars.peek().is_some_and(|(c, _)| *c == '\n');
1938
1939    while let Some((ch, offset)) = chars.next() {
1940        if was_newline && ch == '\n' {
1941            continue;
1942        }
1943        let start_of_next_sentence = if was_newline {
1944            Some(next_non_blank(map, offset))
1945        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1946            Some(next_non_blank(map, offset + ch.len_utf8()))
1947        } else if ch == '.' || ch == '?' || ch == '!' {
1948            start_of_next_sentence(map, offset + ch.len_utf8())
1949        } else {
1950            None
1951        };
1952
1953        if let Some(start_of_next_sentence) = start_of_next_sentence {
1954            times = times.saturating_sub(1);
1955            if times == 0 {
1956                return map.clip_point(
1957                    start_of_next_sentence
1958                        .to_offset(&map.buffer_snapshot)
1959                        .to_display_point(map),
1960                    Bias::Right,
1961                );
1962            }
1963        }
1964
1965        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1966    }
1967
1968    map.max_point()
1969}
1970
1971fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1972    for (c, o) in map.buffer_chars_at(start) {
1973        if c == '\n' || !c.is_whitespace() {
1974            return o;
1975        }
1976    }
1977
1978    map.buffer_snapshot.len()
1979}
1980
1981// given the offset after a ., !, or ? find the start of the next sentence.
1982// if this is not a sentence boundary, returns None.
1983fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1984    let chars = map.buffer_chars_at(end_of_sentence);
1985    let mut seen_space = false;
1986
1987    for (char, offset) in chars {
1988        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1989            continue;
1990        }
1991
1992        if char == '\n' && seen_space {
1993            return Some(offset);
1994        } else if char.is_whitespace() {
1995            seen_space = true;
1996        } else if seen_space {
1997            return Some(offset);
1998        } else {
1999            return None;
2000        }
2001    }
2002
2003    Some(map.buffer_snapshot.len())
2004}
2005
2006fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2007    let point = map.display_point_to_point(display_point, Bias::Left);
2008    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2009        return display_point;
2010    };
2011    let offset = excerpt.buffer().point_to_offset(
2012        excerpt
2013            .buffer()
2014            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2015    );
2016    let buffer_range = excerpt.buffer_range();
2017    if offset >= buffer_range.start && offset <= buffer_range.end {
2018        let point = map
2019            .buffer_snapshot
2020            .offset_to_point(excerpt.map_offset_from_buffer(offset));
2021        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2022    }
2023    let mut last_position = None;
2024    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2025        let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2026            ..language::ToOffset::to_offset(&range.context.end, &buffer);
2027        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2028            let text_anchor = buffer.anchor_after(offset);
2029            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2030            return anchor.to_display_point(map);
2031        } else if offset <= excerpt_range.start {
2032            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2033            return anchor.to_display_point(map);
2034        } else {
2035            last_position = Some(Anchor::in_buffer(
2036                excerpt,
2037                buffer.remote_id(),
2038                range.context.end,
2039            ));
2040        }
2041    }
2042
2043    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2044    last_point.column = point.column;
2045
2046    map.clip_point(
2047        map.point_to_display_point(
2048            map.buffer_snapshot.clip_point(point, Bias::Left),
2049            Bias::Left,
2050        ),
2051        Bias::Left,
2052    )
2053}
2054
2055fn start_of_document(
2056    map: &DisplaySnapshot,
2057    display_point: DisplayPoint,
2058    maybe_times: Option<usize>,
2059) -> DisplayPoint {
2060    if let Some(times) = maybe_times {
2061        return go_to_line(map, display_point, times);
2062    }
2063
2064    let point = map.display_point_to_point(display_point, Bias::Left);
2065    let mut first_point = Point::zero();
2066    first_point.column = point.column;
2067
2068    map.clip_point(
2069        map.point_to_display_point(
2070            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2071            Bias::Left,
2072        ),
2073        Bias::Left,
2074    )
2075}
2076
2077fn end_of_document(
2078    map: &DisplaySnapshot,
2079    display_point: DisplayPoint,
2080    maybe_times: Option<usize>,
2081) -> DisplayPoint {
2082    if let Some(times) = maybe_times {
2083        return go_to_line(map, display_point, times);
2084    };
2085    let point = map.display_point_to_point(display_point, Bias::Left);
2086    let mut last_point = map.buffer_snapshot.max_point();
2087    last_point.column = point.column;
2088
2089    map.clip_point(
2090        map.point_to_display_point(
2091            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2092            Bias::Left,
2093        ),
2094        Bias::Left,
2095    )
2096}
2097
2098fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2099    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2100    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2101
2102    if head > outer.start && head < inner.start {
2103        let mut offset = inner.end.to_offset(map, Bias::Left);
2104        for c in map.buffer_snapshot.chars_at(offset) {
2105            if c == '/' || c == '\n' || c == '>' {
2106                return Some(offset.to_display_point(map));
2107            }
2108            offset += c.len_utf8();
2109        }
2110    } else {
2111        let mut offset = outer.start.to_offset(map, Bias::Left);
2112        for c in map.buffer_snapshot.chars_at(offset) {
2113            offset += c.len_utf8();
2114            if c == '<' || c == '\n' {
2115                return Some(offset.to_display_point(map));
2116            }
2117        }
2118    }
2119
2120    return None;
2121}
2122
2123fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2124    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2125    let display_point = map.clip_at_line_end(display_point);
2126    let point = display_point.to_point(map);
2127    let offset = point.to_offset(&map.buffer_snapshot);
2128
2129    // Ensure the range is contained by the current line.
2130    let mut line_end = map.next_line_boundary(point).0;
2131    if line_end == point {
2132        line_end = map.max_point().to_point(map);
2133    }
2134
2135    let line_range = map.prev_line_boundary(point).0..line_end;
2136    let visible_line_range =
2137        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2138    let ranges = map
2139        .buffer_snapshot
2140        .bracket_ranges(visible_line_range.clone());
2141    if let Some(ranges) = ranges {
2142        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2143            ..line_range.end.to_offset(&map.buffer_snapshot);
2144        let mut closest_pair_destination = None;
2145        let mut closest_distance = usize::MAX;
2146
2147        for (open_range, close_range) in ranges {
2148            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2149                if offset > open_range.start && offset < close_range.start {
2150                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2151                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2152                        return display_point;
2153                    }
2154                    if let Some(tag) = matching_tag(map, display_point) {
2155                        return tag;
2156                    }
2157                } else if close_range.contains(&offset) {
2158                    return open_range.start.to_display_point(map);
2159                } else if open_range.contains(&offset) {
2160                    return (close_range.end - 1).to_display_point(map);
2161                }
2162            }
2163
2164            if (open_range.contains(&offset) || open_range.start >= offset)
2165                && line_range.contains(&open_range.start)
2166            {
2167                let distance = open_range.start.saturating_sub(offset);
2168                if distance < closest_distance {
2169                    closest_pair_destination = Some(close_range.start);
2170                    closest_distance = distance;
2171                    continue;
2172                }
2173            }
2174
2175            if (close_range.contains(&offset) || close_range.start >= offset)
2176                && line_range.contains(&close_range.start)
2177            {
2178                let distance = close_range.start.saturating_sub(offset);
2179                if distance < closest_distance {
2180                    closest_pair_destination = Some(open_range.start);
2181                    closest_distance = distance;
2182                    continue;
2183                }
2184            }
2185
2186            continue;
2187        }
2188
2189        closest_pair_destination
2190            .map(|destination| destination.to_display_point(map))
2191            .unwrap_or(display_point)
2192    } else {
2193        display_point
2194    }
2195}
2196
2197fn unmatched_forward(
2198    map: &DisplaySnapshot,
2199    mut display_point: DisplayPoint,
2200    char: char,
2201    times: usize,
2202) -> DisplayPoint {
2203    for _ in 0..times {
2204        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2205        let point = display_point.to_point(map);
2206        let offset = point.to_offset(&map.buffer_snapshot);
2207
2208        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2209        let Some(ranges) = ranges else { break };
2210        let mut closest_closing_destination = None;
2211        let mut closest_distance = usize::MAX;
2212
2213        for (_, close_range) in ranges {
2214            if close_range.start > offset {
2215                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2216                if Some(char) == chars.next() {
2217                    let distance = close_range.start - offset;
2218                    if distance < closest_distance {
2219                        closest_closing_destination = Some(close_range.start);
2220                        closest_distance = distance;
2221                        continue;
2222                    }
2223                }
2224            }
2225        }
2226
2227        let new_point = closest_closing_destination
2228            .map(|destination| destination.to_display_point(map))
2229            .unwrap_or(display_point);
2230        if new_point == display_point {
2231            break;
2232        }
2233        display_point = new_point;
2234    }
2235    return display_point;
2236}
2237
2238fn unmatched_backward(
2239    map: &DisplaySnapshot,
2240    mut display_point: DisplayPoint,
2241    char: char,
2242    times: usize,
2243) -> DisplayPoint {
2244    for _ in 0..times {
2245        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2246        let point = display_point.to_point(map);
2247        let offset = point.to_offset(&map.buffer_snapshot);
2248
2249        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2250        let Some(ranges) = ranges else {
2251            break;
2252        };
2253
2254        let mut closest_starting_destination = None;
2255        let mut closest_distance = usize::MAX;
2256
2257        for (start_range, _) in ranges {
2258            if start_range.start < offset {
2259                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2260                if Some(char) == chars.next() {
2261                    let distance = offset - start_range.start;
2262                    if distance < closest_distance {
2263                        closest_starting_destination = Some(start_range.start);
2264                        closest_distance = distance;
2265                        continue;
2266                    }
2267                }
2268            }
2269        }
2270
2271        let new_point = closest_starting_destination
2272            .map(|destination| destination.to_display_point(map))
2273            .unwrap_or(display_point);
2274        if new_point == display_point {
2275            break;
2276        } else {
2277            display_point = new_point;
2278        }
2279    }
2280    display_point
2281}
2282
2283fn find_forward(
2284    map: &DisplaySnapshot,
2285    from: DisplayPoint,
2286    before: bool,
2287    target: char,
2288    times: usize,
2289    mode: FindRange,
2290    smartcase: bool,
2291) -> Option<DisplayPoint> {
2292    let mut to = from;
2293    let mut found = false;
2294
2295    for _ in 0..times {
2296        found = false;
2297        let new_to = find_boundary(map, to, mode, |_, right| {
2298            found = is_character_match(target, right, smartcase);
2299            found
2300        });
2301        if to == new_to {
2302            break;
2303        }
2304        to = new_to;
2305    }
2306
2307    if found {
2308        if before && to.column() > 0 {
2309            *to.column_mut() -= 1;
2310            Some(map.clip_point(to, Bias::Left))
2311        } else {
2312            Some(to)
2313        }
2314    } else {
2315        None
2316    }
2317}
2318
2319fn find_backward(
2320    map: &DisplaySnapshot,
2321    from: DisplayPoint,
2322    after: bool,
2323    target: char,
2324    times: usize,
2325    mode: FindRange,
2326    smartcase: bool,
2327) -> DisplayPoint {
2328    let mut to = from;
2329
2330    for _ in 0..times {
2331        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2332            is_character_match(target, right, smartcase)
2333        });
2334        if to == new_to {
2335            break;
2336        }
2337        to = new_to;
2338    }
2339
2340    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2341    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2342        if after {
2343            *to.column_mut() += 1;
2344            map.clip_point(to, Bias::Right)
2345        } else {
2346            to
2347        }
2348    } else {
2349        from
2350    }
2351}
2352
2353fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2354    if smartcase {
2355        if target.is_uppercase() {
2356            target == other
2357        } else {
2358            target == other.to_ascii_lowercase()
2359        }
2360    } else {
2361        target == other
2362    }
2363}
2364
2365fn sneak(
2366    map: &DisplaySnapshot,
2367    from: DisplayPoint,
2368    first_target: char,
2369    second_target: char,
2370    times: usize,
2371    smartcase: bool,
2372) -> Option<DisplayPoint> {
2373    let mut to = from;
2374    let mut found = false;
2375
2376    for _ in 0..times {
2377        found = false;
2378        let new_to = find_boundary(
2379            map,
2380            movement::right(map, to),
2381            FindRange::MultiLine,
2382            |left, right| {
2383                found = is_character_match(first_target, left, smartcase)
2384                    && is_character_match(second_target, right, smartcase);
2385                found
2386            },
2387        );
2388        if to == new_to {
2389            break;
2390        }
2391        to = new_to;
2392    }
2393
2394    if found {
2395        Some(movement::left(map, to))
2396    } else {
2397        None
2398    }
2399}
2400
2401fn sneak_backward(
2402    map: &DisplaySnapshot,
2403    from: DisplayPoint,
2404    first_target: char,
2405    second_target: char,
2406    times: usize,
2407    smartcase: bool,
2408) -> Option<DisplayPoint> {
2409    let mut to = from;
2410    let mut found = false;
2411
2412    for _ in 0..times {
2413        found = false;
2414        let new_to =
2415            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2416                found = is_character_match(first_target, left, smartcase)
2417                    && is_character_match(second_target, right, smartcase);
2418                found
2419            });
2420        if to == new_to {
2421            break;
2422        }
2423        to = new_to;
2424    }
2425
2426    if found {
2427        Some(movement::left(map, to))
2428    } else {
2429        None
2430    }
2431}
2432
2433fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2434    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2435    first_non_whitespace(map, false, correct_line)
2436}
2437
2438fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2439    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2440    first_non_whitespace(map, false, correct_line)
2441}
2442
2443fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2444    let correct_line = start_of_relative_buffer_row(map, point, 0);
2445    right(map, correct_line, times.saturating_sub(1))
2446}
2447
2448pub(crate) fn next_line_end(
2449    map: &DisplaySnapshot,
2450    mut point: DisplayPoint,
2451    times: usize,
2452) -> DisplayPoint {
2453    if times > 1 {
2454        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2455    }
2456    end_of_line(map, false, point, 1)
2457}
2458
2459fn window_top(
2460    map: &DisplaySnapshot,
2461    point: DisplayPoint,
2462    text_layout_details: &TextLayoutDetails,
2463    mut times: usize,
2464) -> (DisplayPoint, SelectionGoal) {
2465    let first_visible_line = text_layout_details
2466        .scroll_anchor
2467        .anchor
2468        .to_display_point(map);
2469
2470    if first_visible_line.row() != DisplayRow(0)
2471        && text_layout_details.vertical_scroll_margin as usize > times
2472    {
2473        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2474    }
2475
2476    if let Some(visible_rows) = text_layout_details.visible_rows {
2477        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2478        let new_row = (first_visible_line.row().0 + (times as u32))
2479            .min(bottom_row)
2480            .min(map.max_point().row().0);
2481        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2482
2483        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2484        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2485    } else {
2486        let new_row =
2487            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2488        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2489
2490        let new_point = DisplayPoint::new(new_row, new_col);
2491        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2492    }
2493}
2494
2495fn window_middle(
2496    map: &DisplaySnapshot,
2497    point: DisplayPoint,
2498    text_layout_details: &TextLayoutDetails,
2499) -> (DisplayPoint, SelectionGoal) {
2500    if let Some(visible_rows) = text_layout_details.visible_rows {
2501        let first_visible_line = text_layout_details
2502            .scroll_anchor
2503            .anchor
2504            .to_display_point(map);
2505
2506        let max_visible_rows =
2507            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2508
2509        let new_row =
2510            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2511        let new_row = DisplayRow(new_row);
2512        let new_col = point.column().min(map.line_len(new_row));
2513        let new_point = DisplayPoint::new(new_row, new_col);
2514        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2515    } else {
2516        (point, SelectionGoal::None)
2517    }
2518}
2519
2520fn window_bottom(
2521    map: &DisplaySnapshot,
2522    point: DisplayPoint,
2523    text_layout_details: &TextLayoutDetails,
2524    mut times: usize,
2525) -> (DisplayPoint, SelectionGoal) {
2526    if let Some(visible_rows) = text_layout_details.visible_rows {
2527        let first_visible_line = text_layout_details
2528            .scroll_anchor
2529            .anchor
2530            .to_display_point(map);
2531        let bottom_row = first_visible_line.row().0
2532            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2533        if bottom_row < map.max_point().row().0
2534            && text_layout_details.vertical_scroll_margin as usize > times
2535        {
2536            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2537        }
2538        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2539        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2540        {
2541            first_visible_line.row()
2542        } else {
2543            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2544        };
2545        let new_col = point.column().min(map.line_len(new_row));
2546        let new_point = DisplayPoint::new(new_row, new_col);
2547        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2548    } else {
2549        (point, SelectionGoal::None)
2550    }
2551}
2552
2553fn method_motion(
2554    map: &DisplaySnapshot,
2555    mut display_point: DisplayPoint,
2556    times: usize,
2557    direction: Direction,
2558    is_start: bool,
2559) -> DisplayPoint {
2560    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2561        return display_point;
2562    };
2563
2564    for _ in 0..times {
2565        let point = map.display_point_to_point(display_point, Bias::Left);
2566        let offset = point.to_offset(&map.buffer_snapshot);
2567        let range = if direction == Direction::Prev {
2568            0..offset
2569        } else {
2570            offset..buffer.len()
2571        };
2572
2573        let possibilities = buffer
2574            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2575            .filter_map(|(range, object)| {
2576                if !matches!(object, language::TextObject::AroundFunction) {
2577                    return None;
2578                }
2579
2580                let relevant = if is_start { range.start } else { range.end };
2581                if direction == Direction::Prev && relevant < offset {
2582                    Some(relevant)
2583                } else if direction == Direction::Next && relevant > offset + 1 {
2584                    Some(relevant)
2585                } else {
2586                    None
2587                }
2588            });
2589
2590        let dest = if direction == Direction::Prev {
2591            possibilities.max().unwrap_or(offset)
2592        } else {
2593            possibilities.min().unwrap_or(offset)
2594        };
2595        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2596        if new_point == display_point {
2597            break;
2598        }
2599        display_point = new_point;
2600    }
2601    display_point
2602}
2603
2604fn comment_motion(
2605    map: &DisplaySnapshot,
2606    mut display_point: DisplayPoint,
2607    times: usize,
2608    direction: Direction,
2609) -> DisplayPoint {
2610    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2611        return display_point;
2612    };
2613
2614    for _ in 0..times {
2615        let point = map.display_point_to_point(display_point, Bias::Left);
2616        let offset = point.to_offset(&map.buffer_snapshot);
2617        let range = if direction == Direction::Prev {
2618            0..offset
2619        } else {
2620            offset..buffer.len()
2621        };
2622
2623        let possibilities = buffer
2624            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2625            .filter_map(|(range, object)| {
2626                if !matches!(object, language::TextObject::AroundComment) {
2627                    return None;
2628                }
2629
2630                let relevant = if direction == Direction::Prev {
2631                    range.start
2632                } else {
2633                    range.end
2634                };
2635                if direction == Direction::Prev && relevant < offset {
2636                    Some(relevant)
2637                } else if direction == Direction::Next && relevant > offset + 1 {
2638                    Some(relevant)
2639                } else {
2640                    None
2641                }
2642            });
2643
2644        let dest = if direction == Direction::Prev {
2645            possibilities.max().unwrap_or(offset)
2646        } else {
2647            possibilities.min().unwrap_or(offset)
2648        };
2649        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2650        if new_point == display_point {
2651            break;
2652        }
2653        display_point = new_point;
2654    }
2655
2656    display_point
2657}
2658
2659fn section_motion(
2660    map: &DisplaySnapshot,
2661    mut display_point: DisplayPoint,
2662    times: usize,
2663    direction: Direction,
2664    is_start: bool,
2665) -> DisplayPoint {
2666    if map.buffer_snapshot.as_singleton().is_some() {
2667        for _ in 0..times {
2668            let offset = map
2669                .display_point_to_point(display_point, Bias::Left)
2670                .to_offset(&map.buffer_snapshot);
2671            let range = if direction == Direction::Prev {
2672                0..offset
2673            } else {
2674                offset..map.buffer_snapshot.len()
2675            };
2676
2677            // we set a max start depth here because we want a section to only be "top level"
2678            // similar to vim's default of '{' in the first column.
2679            // (and without it, ]] at the start of editor.rs is -very- slow)
2680            let mut possibilities = map
2681                .buffer_snapshot
2682                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2683                .filter(|(_, object)| {
2684                    matches!(
2685                        object,
2686                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2687                    )
2688                })
2689                .collect::<Vec<_>>();
2690            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2691            let mut prev_end = None;
2692            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2693                if t == language::TextObject::AroundFunction
2694                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2695                {
2696                    return None;
2697                }
2698                prev_end = Some(range.end);
2699
2700                let relevant = if is_start { range.start } else { range.end };
2701                if direction == Direction::Prev && relevant < offset {
2702                    Some(relevant)
2703                } else if direction == Direction::Next && relevant > offset + 1 {
2704                    Some(relevant)
2705                } else {
2706                    None
2707                }
2708            });
2709
2710            let offset = if direction == Direction::Prev {
2711                possibilities.max().unwrap_or(0)
2712            } else {
2713                possibilities.min().unwrap_or(map.buffer_snapshot.len())
2714            };
2715
2716            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2717            if new_point == display_point {
2718                break;
2719            }
2720            display_point = new_point;
2721        }
2722        return display_point;
2723    };
2724
2725    for _ in 0..times {
2726        let next_point = if is_start {
2727            movement::start_of_excerpt(map, display_point, direction)
2728        } else {
2729            movement::end_of_excerpt(map, display_point, direction)
2730        };
2731        if next_point == display_point {
2732            break;
2733        }
2734        display_point = next_point;
2735    }
2736
2737    display_point
2738}
2739
2740#[cfg(test)]
2741mod test {
2742
2743    use crate::{
2744        state::Mode,
2745        test::{NeovimBackedTestContext, VimTestContext},
2746    };
2747    use editor::display_map::Inlay;
2748    use indoc::indoc;
2749    use language::Point;
2750    use multi_buffer::MultiBufferRow;
2751
2752    #[gpui::test]
2753    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2754        let mut cx = NeovimBackedTestContext::new(cx).await;
2755
2756        let initial_state = indoc! {r"ˇabc
2757            def
2758
2759            paragraph
2760            the second
2761
2762
2763
2764            third and
2765            final"};
2766
2767        // goes down once
2768        cx.set_shared_state(initial_state).await;
2769        cx.simulate_shared_keystrokes("}").await;
2770        cx.shared_state().await.assert_eq(indoc! {r"abc
2771            def
2772            ˇ
2773            paragraph
2774            the second
2775
2776
2777
2778            third and
2779            final"});
2780
2781        // goes up once
2782        cx.simulate_shared_keystrokes("{").await;
2783        cx.shared_state().await.assert_eq(initial_state);
2784
2785        // goes down twice
2786        cx.simulate_shared_keystrokes("2 }").await;
2787        cx.shared_state().await.assert_eq(indoc! {r"abc
2788            def
2789
2790            paragraph
2791            the second
2792            ˇ
2793
2794
2795            third and
2796            final"});
2797
2798        // goes down over multiple blanks
2799        cx.simulate_shared_keystrokes("}").await;
2800        cx.shared_state().await.assert_eq(indoc! {r"abc
2801                def
2802
2803                paragraph
2804                the second
2805
2806
2807
2808                third and
2809                finaˇl"});
2810
2811        // goes up twice
2812        cx.simulate_shared_keystrokes("2 {").await;
2813        cx.shared_state().await.assert_eq(indoc! {r"abc
2814                def
2815                ˇ
2816                paragraph
2817                the second
2818
2819
2820
2821                third and
2822                final"});
2823    }
2824
2825    #[gpui::test]
2826    async fn test_matching(cx: &mut gpui::TestAppContext) {
2827        let mut cx = NeovimBackedTestContext::new(cx).await;
2828
2829        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2830                do(something(with<Types>.and_arrays[0, 2]))
2831            }"})
2832            .await;
2833        cx.simulate_shared_keystrokes("%").await;
2834        cx.shared_state()
2835            .await
2836            .assert_eq(indoc! {r"func (a stringˇ) {
2837                do(something(with<Types>.and_arrays[0, 2]))
2838            }"});
2839
2840        // test it works on the last character of the line
2841        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2842            do(something(with<Types>.and_arrays[0, 2]))
2843            }"})
2844            .await;
2845        cx.simulate_shared_keystrokes("%").await;
2846        cx.shared_state()
2847            .await
2848            .assert_eq(indoc! {r"func (a string) {
2849            do(something(with<Types>.and_arrays[0, 2]))
2850            ˇ}"});
2851
2852        // test it works on immediate nesting
2853        cx.set_shared_state("ˇ{()}").await;
2854        cx.simulate_shared_keystrokes("%").await;
2855        cx.shared_state().await.assert_eq("{()ˇ}");
2856        cx.simulate_shared_keystrokes("%").await;
2857        cx.shared_state().await.assert_eq("ˇ{()}");
2858
2859        // test it works on immediate nesting inside braces
2860        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2861        cx.simulate_shared_keystrokes("%").await;
2862        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2863
2864        // test it jumps to the next paren on a line
2865        cx.set_shared_state("func ˇboop() {\n}").await;
2866        cx.simulate_shared_keystrokes("%").await;
2867        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2868    }
2869
2870    #[gpui::test]
2871    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2872        let mut cx = NeovimBackedTestContext::new(cx).await;
2873
2874        // test it works with curly braces
2875        cx.set_shared_state(indoc! {r"func (a string) {
2876                do(something(with<Types>.anˇd_arrays[0, 2]))
2877            }"})
2878            .await;
2879        cx.simulate_shared_keystrokes("] }").await;
2880        cx.shared_state()
2881            .await
2882            .assert_eq(indoc! {r"func (a string) {
2883                do(something(with<Types>.and_arrays[0, 2]))
2884            ˇ}"});
2885
2886        // test it works with brackets
2887        cx.set_shared_state(indoc! {r"func (a string) {
2888                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2889            }"})
2890            .await;
2891        cx.simulate_shared_keystrokes("] )").await;
2892        cx.shared_state()
2893            .await
2894            .assert_eq(indoc! {r"func (a string) {
2895                do(something(with<Types>.and_arrays[0, 2])ˇ)
2896            }"});
2897
2898        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2899            .await;
2900        cx.simulate_shared_keystrokes("] )").await;
2901        cx.shared_state()
2902            .await
2903            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2904
2905        // test it works on immediate nesting
2906        cx.set_shared_state("{ˇ {}{}}").await;
2907        cx.simulate_shared_keystrokes("] }").await;
2908        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2909        cx.set_shared_state("(ˇ ()())").await;
2910        cx.simulate_shared_keystrokes("] )").await;
2911        cx.shared_state().await.assert_eq("( ()()ˇ)");
2912
2913        // test it works on immediate nesting inside braces
2914        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2915        cx.simulate_shared_keystrokes("] }").await;
2916        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2917        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2918        cx.simulate_shared_keystrokes("] )").await;
2919        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2920    }
2921
2922    #[gpui::test]
2923    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2924        let mut cx = NeovimBackedTestContext::new(cx).await;
2925
2926        // test it works with curly braces
2927        cx.set_shared_state(indoc! {r"func (a string) {
2928                do(something(with<Types>.anˇd_arrays[0, 2]))
2929            }"})
2930            .await;
2931        cx.simulate_shared_keystrokes("[ {").await;
2932        cx.shared_state()
2933            .await
2934            .assert_eq(indoc! {r"func (a string) ˇ{
2935                do(something(with<Types>.and_arrays[0, 2]))
2936            }"});
2937
2938        // test it works with brackets
2939        cx.set_shared_state(indoc! {r"func (a string) {
2940                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2941            }"})
2942            .await;
2943        cx.simulate_shared_keystrokes("[ (").await;
2944        cx.shared_state()
2945            .await
2946            .assert_eq(indoc! {r"func (a string) {
2947                doˇ(something(with<Types>.and_arrays[0, 2]))
2948            }"});
2949
2950        // test it works on immediate nesting
2951        cx.set_shared_state("{{}{} ˇ }").await;
2952        cx.simulate_shared_keystrokes("[ {").await;
2953        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2954        cx.set_shared_state("(()() ˇ )").await;
2955        cx.simulate_shared_keystrokes("[ (").await;
2956        cx.shared_state().await.assert_eq("ˇ(()()  )");
2957
2958        // test it works on immediate nesting inside braces
2959        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2960        cx.simulate_shared_keystrokes("[ {").await;
2961        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2962        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2963        cx.simulate_shared_keystrokes("[ (").await;
2964        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2965    }
2966
2967    #[gpui::test]
2968    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2969        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2970
2971        cx.neovim.exec("set filetype=html").await;
2972
2973        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2974        cx.simulate_shared_keystrokes("%").await;
2975        cx.shared_state()
2976            .await
2977            .assert_eq(indoc! {r"<body><ˇ/body>"});
2978        cx.simulate_shared_keystrokes("%").await;
2979
2980        // test jumping backwards
2981        cx.shared_state()
2982            .await
2983            .assert_eq(indoc! {r"<ˇbody></body>"});
2984
2985        // test self-closing tags
2986        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2987        cx.simulate_shared_keystrokes("%").await;
2988        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2989
2990        // test tag with attributes
2991        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2992            </div>
2993            "})
2994            .await;
2995        cx.simulate_shared_keystrokes("%").await;
2996        cx.shared_state()
2997            .await
2998            .assert_eq(indoc! {r"<div class='test' id='main'>
2999            <ˇ/div>
3000            "});
3001
3002        // test multi-line self-closing tag
3003        cx.set_shared_state(indoc! {r#"<a>
3004            <br
3005                test = "test"
3006            /ˇ>
3007        </a>"#})
3008            .await;
3009        cx.simulate_shared_keystrokes("%").await;
3010        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3011            ˇ<br
3012                test = "test"
3013            />
3014        </a>"#});
3015    }
3016
3017    #[gpui::test]
3018    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3019        let mut cx = NeovimBackedTestContext::new(cx).await;
3020
3021        // f and F
3022        cx.set_shared_state("ˇone two three four").await;
3023        cx.simulate_shared_keystrokes("f o").await;
3024        cx.shared_state().await.assert_eq("one twˇo three four");
3025        cx.simulate_shared_keystrokes(",").await;
3026        cx.shared_state().await.assert_eq("ˇone two three four");
3027        cx.simulate_shared_keystrokes("2 ;").await;
3028        cx.shared_state().await.assert_eq("one two three fˇour");
3029        cx.simulate_shared_keystrokes("shift-f e").await;
3030        cx.shared_state().await.assert_eq("one two threˇe four");
3031        cx.simulate_shared_keystrokes("2 ;").await;
3032        cx.shared_state().await.assert_eq("onˇe two three four");
3033        cx.simulate_shared_keystrokes(",").await;
3034        cx.shared_state().await.assert_eq("one two thrˇee four");
3035
3036        // t and T
3037        cx.set_shared_state("ˇone two three four").await;
3038        cx.simulate_shared_keystrokes("t o").await;
3039        cx.shared_state().await.assert_eq("one tˇwo three four");
3040        cx.simulate_shared_keystrokes(",").await;
3041        cx.shared_state().await.assert_eq("oˇne two three four");
3042        cx.simulate_shared_keystrokes("2 ;").await;
3043        cx.shared_state().await.assert_eq("one two three ˇfour");
3044        cx.simulate_shared_keystrokes("shift-t e").await;
3045        cx.shared_state().await.assert_eq("one two threeˇ four");
3046        cx.simulate_shared_keystrokes("3 ;").await;
3047        cx.shared_state().await.assert_eq("oneˇ two three four");
3048        cx.simulate_shared_keystrokes(",").await;
3049        cx.shared_state().await.assert_eq("one two thˇree four");
3050    }
3051
3052    #[gpui::test]
3053    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3054        let mut cx = NeovimBackedTestContext::new(cx).await;
3055        let initial_state = indoc! {r"something(ˇfoo)"};
3056        cx.set_shared_state(initial_state).await;
3057        cx.simulate_shared_keystrokes("}").await;
3058        cx.shared_state().await.assert_eq("something(fooˇ)");
3059    }
3060
3061    #[gpui::test]
3062    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3063        let mut cx = NeovimBackedTestContext::new(cx).await;
3064        cx.set_shared_state("ˇone\n  two\nthree").await;
3065        cx.simulate_shared_keystrokes("enter").await;
3066        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3067    }
3068
3069    #[gpui::test]
3070    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3071        let mut cx = NeovimBackedTestContext::new(cx).await;
3072        cx.set_shared_state("ˇ one\n two \nthree").await;
3073        cx.simulate_shared_keystrokes("g _").await;
3074        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3075
3076        cx.set_shared_state("ˇ one \n two \nthree").await;
3077        cx.simulate_shared_keystrokes("g _").await;
3078        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3079        cx.simulate_shared_keystrokes("2 g _").await;
3080        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3081    }
3082
3083    #[gpui::test]
3084    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3085        let mut cx = NeovimBackedTestContext::new(cx).await;
3086        let initial_state = indoc! {r"abc
3087          def
3088          paragraph
3089          the second
3090          third ˇand
3091          final"};
3092
3093        cx.set_shared_state(initial_state).await;
3094        cx.simulate_shared_keystrokes("shift-h").await;
3095        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3096          def
3097          paragraph
3098          the second
3099          third and
3100          final"});
3101
3102        // clip point
3103        cx.set_shared_state(indoc! {r"
3104          1 2 3
3105          4 5 6
3106          7 8 ˇ9
3107          "})
3108            .await;
3109        cx.simulate_shared_keystrokes("shift-h").await;
3110        cx.shared_state().await.assert_eq(indoc! {"
3111          1 2 ˇ3
3112          4 5 6
3113          7 8 9
3114          "});
3115
3116        cx.set_shared_state(indoc! {r"
3117          1 2 3
3118          4 5 6
3119          ˇ7 8 9
3120          "})
3121            .await;
3122        cx.simulate_shared_keystrokes("shift-h").await;
3123        cx.shared_state().await.assert_eq(indoc! {"
3124          ˇ1 2 3
3125          4 5 6
3126          7 8 9
3127          "});
3128
3129        cx.set_shared_state(indoc! {r"
3130          1 2 3
3131          4 5 ˇ6
3132          7 8 9"})
3133            .await;
3134        cx.simulate_shared_keystrokes("9 shift-h").await;
3135        cx.shared_state().await.assert_eq(indoc! {"
3136          1 2 3
3137          4 5 6
3138          7 8 ˇ9"});
3139    }
3140
3141    #[gpui::test]
3142    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3143        let mut cx = NeovimBackedTestContext::new(cx).await;
3144        let initial_state = indoc! {r"abˇc
3145          def
3146          paragraph
3147          the second
3148          third and
3149          final"};
3150
3151        cx.set_shared_state(initial_state).await;
3152        cx.simulate_shared_keystrokes("shift-m").await;
3153        cx.shared_state().await.assert_eq(indoc! {r"abc
3154          def
3155          paˇragraph
3156          the second
3157          third and
3158          final"});
3159
3160        cx.set_shared_state(indoc! {r"
3161          1 2 3
3162          4 5 6
3163          7 8 ˇ9
3164          "})
3165            .await;
3166        cx.simulate_shared_keystrokes("shift-m").await;
3167        cx.shared_state().await.assert_eq(indoc! {"
3168          1 2 3
3169          4 5 ˇ6
3170          7 8 9
3171          "});
3172        cx.set_shared_state(indoc! {r"
3173          1 2 3
3174          4 5 6
3175          ˇ7 8 9
3176          "})
3177            .await;
3178        cx.simulate_shared_keystrokes("shift-m").await;
3179        cx.shared_state().await.assert_eq(indoc! {"
3180          1 2 3
3181          ˇ4 5 6
3182          7 8 9
3183          "});
3184        cx.set_shared_state(indoc! {r"
3185          ˇ1 2 3
3186          4 5 6
3187          7 8 9
3188          "})
3189            .await;
3190        cx.simulate_shared_keystrokes("shift-m").await;
3191        cx.shared_state().await.assert_eq(indoc! {"
3192          1 2 3
3193          ˇ4 5 6
3194          7 8 9
3195          "});
3196        cx.set_shared_state(indoc! {r"
3197          1 2 3
3198          ˇ4 5 6
3199          7 8 9
3200          "})
3201            .await;
3202        cx.simulate_shared_keystrokes("shift-m").await;
3203        cx.shared_state().await.assert_eq(indoc! {"
3204          1 2 3
3205          ˇ4 5 6
3206          7 8 9
3207          "});
3208        cx.set_shared_state(indoc! {r"
3209          1 2 3
3210          4 5 ˇ6
3211          7 8 9
3212          "})
3213            .await;
3214        cx.simulate_shared_keystrokes("shift-m").await;
3215        cx.shared_state().await.assert_eq(indoc! {"
3216          1 2 3
3217          4 5 ˇ6
3218          7 8 9
3219          "});
3220    }
3221
3222    #[gpui::test]
3223    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3224        let mut cx = NeovimBackedTestContext::new(cx).await;
3225        let initial_state = indoc! {r"abc
3226          deˇf
3227          paragraph
3228          the second
3229          third and
3230          final"};
3231
3232        cx.set_shared_state(initial_state).await;
3233        cx.simulate_shared_keystrokes("shift-l").await;
3234        cx.shared_state().await.assert_eq(indoc! {r"abc
3235          def
3236          paragraph
3237          the second
3238          third and
3239          fiˇnal"});
3240
3241        cx.set_shared_state(indoc! {r"
3242          1 2 3
3243          4 5 ˇ6
3244          7 8 9
3245          "})
3246            .await;
3247        cx.simulate_shared_keystrokes("shift-l").await;
3248        cx.shared_state().await.assert_eq(indoc! {"
3249          1 2 3
3250          4 5 6
3251          7 8 9
3252          ˇ"});
3253
3254        cx.set_shared_state(indoc! {r"
3255          1 2 3
3256          ˇ4 5 6
3257          7 8 9
3258          "})
3259            .await;
3260        cx.simulate_shared_keystrokes("shift-l").await;
3261        cx.shared_state().await.assert_eq(indoc! {"
3262          1 2 3
3263          4 5 6
3264          7 8 9
3265          ˇ"});
3266
3267        cx.set_shared_state(indoc! {r"
3268          1 2 ˇ3
3269          4 5 6
3270          7 8 9
3271          "})
3272            .await;
3273        cx.simulate_shared_keystrokes("shift-l").await;
3274        cx.shared_state().await.assert_eq(indoc! {"
3275          1 2 3
3276          4 5 6
3277          7 8 9
3278          ˇ"});
3279
3280        cx.set_shared_state(indoc! {r"
3281          ˇ1 2 3
3282          4 5 6
3283          7 8 9
3284          "})
3285            .await;
3286        cx.simulate_shared_keystrokes("shift-l").await;
3287        cx.shared_state().await.assert_eq(indoc! {"
3288          1 2 3
3289          4 5 6
3290          7 8 9
3291          ˇ"});
3292
3293        cx.set_shared_state(indoc! {r"
3294          1 2 3
3295          4 5 ˇ6
3296          7 8 9
3297          "})
3298            .await;
3299        cx.simulate_shared_keystrokes("9 shift-l").await;
3300        cx.shared_state().await.assert_eq(indoc! {"
3301          1 2 ˇ3
3302          4 5 6
3303          7 8 9
3304          "});
3305    }
3306
3307    #[gpui::test]
3308    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3309        let mut cx = NeovimBackedTestContext::new(cx).await;
3310        cx.set_shared_state(indoc! {r"
3311        456 5ˇ67 678
3312        "})
3313            .await;
3314        cx.simulate_shared_keystrokes("g e").await;
3315        cx.shared_state().await.assert_eq(indoc! {"
3316        45ˇ6 567 678
3317        "});
3318
3319        // Test times
3320        cx.set_shared_state(indoc! {r"
3321        123 234 345
3322        456 5ˇ67 678
3323        "})
3324            .await;
3325        cx.simulate_shared_keystrokes("4 g e").await;
3326        cx.shared_state().await.assert_eq(indoc! {"
3327        12ˇ3 234 345
3328        456 567 678
3329        "});
3330
3331        // With punctuation
3332        cx.set_shared_state(indoc! {r"
3333        123 234 345
3334        4;5.6 5ˇ67 678
3335        789 890 901
3336        "})
3337            .await;
3338        cx.simulate_shared_keystrokes("g e").await;
3339        cx.shared_state().await.assert_eq(indoc! {"
3340          123 234 345
3341          4;5.ˇ6 567 678
3342          789 890 901
3343        "});
3344
3345        // With punctuation and count
3346        cx.set_shared_state(indoc! {r"
3347        123 234 345
3348        4;5.6 5ˇ67 678
3349        789 890 901
3350        "})
3351            .await;
3352        cx.simulate_shared_keystrokes("5 g e").await;
3353        cx.shared_state().await.assert_eq(indoc! {"
3354          123 234 345
3355          ˇ4;5.6 567 678
3356          789 890 901
3357        "});
3358
3359        // newlines
3360        cx.set_shared_state(indoc! {r"
3361        123 234 345
3362
3363        78ˇ9 890 901
3364        "})
3365            .await;
3366        cx.simulate_shared_keystrokes("g e").await;
3367        cx.shared_state().await.assert_eq(indoc! {"
3368          123 234 345
3369          ˇ
3370          789 890 901
3371        "});
3372        cx.simulate_shared_keystrokes("g e").await;
3373        cx.shared_state().await.assert_eq(indoc! {"
3374          123 234 34ˇ5
3375
3376          789 890 901
3377        "});
3378
3379        // With punctuation
3380        cx.set_shared_state(indoc! {r"
3381        123 234 345
3382        4;5.ˇ6 567 678
3383        789 890 901
3384        "})
3385            .await;
3386        cx.simulate_shared_keystrokes("g shift-e").await;
3387        cx.shared_state().await.assert_eq(indoc! {"
3388          123 234 34ˇ5
3389          4;5.6 567 678
3390          789 890 901
3391        "});
3392    }
3393
3394    #[gpui::test]
3395    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3396        let mut cx = NeovimBackedTestContext::new(cx).await;
3397
3398        cx.set_shared_state(indoc! {"
3399            fn aˇ() {
3400              return
3401            }
3402        "})
3403            .await;
3404        cx.simulate_shared_keystrokes("v $ %").await;
3405        cx.shared_state().await.assert_eq(indoc! {"
3406            fn a«() {
3407              return
3408            }ˇ»
3409        "});
3410    }
3411
3412    #[gpui::test]
3413    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3414        let mut cx = VimTestContext::new(cx, true).await;
3415
3416        cx.set_state(
3417            indoc! {"
3418                struct Foo {
3419                ˇ
3420                }
3421            "},
3422            Mode::Normal,
3423        );
3424
3425        cx.update_editor(|editor, _window, cx| {
3426            let range = editor.selections.newest_anchor().range();
3427            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3428            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3429            editor.splice_inlays(&[], vec![inlay], cx);
3430        });
3431
3432        cx.simulate_keystrokes("j");
3433        cx.assert_state(
3434            indoc! {"
3435                struct Foo {
3436
3437                ˇ}
3438            "},
3439            Mode::Normal,
3440        );
3441    }
3442
3443    #[gpui::test]
3444    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3445        let mut cx = VimTestContext::new(cx, true).await;
3446
3447        cx.set_state(
3448            indoc! {"
3449            ˇstruct Foo {
3450
3451            }
3452        "},
3453            Mode::Normal,
3454        );
3455        cx.update_editor(|editor, _window, cx| {
3456            let snapshot = editor.buffer().read(cx).snapshot(cx);
3457            let end_of_line =
3458                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3459            let inlay_text = " hint";
3460            let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3461            editor.splice_inlays(&[], vec![inlay], cx);
3462        });
3463        cx.simulate_keystrokes("$");
3464        cx.assert_state(
3465            indoc! {"
3466            struct Foo ˇ{
3467
3468            }
3469        "},
3470            Mode::Normal,
3471        );
3472    }
3473}