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