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 next_point = if is_start {
2703            movement::start_of_excerpt(map, display_point, direction)
2704        } else {
2705            movement::end_of_excerpt(map, display_point, direction)
2706        };
2707        if next_point == display_point {
2708            break;
2709        }
2710        display_point = next_point;
2711    }
2712
2713    display_point
2714}
2715
2716#[cfg(test)]
2717mod test {
2718
2719    use crate::{
2720        state::Mode,
2721        test::{NeovimBackedTestContext, VimTestContext},
2722    };
2723    use editor::display_map::Inlay;
2724    use indoc::indoc;
2725    use language::Point;
2726    use multi_buffer::MultiBufferRow;
2727
2728    #[gpui::test]
2729    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2730        let mut cx = NeovimBackedTestContext::new(cx).await;
2731
2732        let initial_state = indoc! {r"ˇabc
2733            def
2734
2735            paragraph
2736            the second
2737
2738
2739
2740            third and
2741            final"};
2742
2743        // goes down once
2744        cx.set_shared_state(initial_state).await;
2745        cx.simulate_shared_keystrokes("}").await;
2746        cx.shared_state().await.assert_eq(indoc! {r"abc
2747            def
2748            ˇ
2749            paragraph
2750            the second
2751
2752
2753
2754            third and
2755            final"});
2756
2757        // goes up once
2758        cx.simulate_shared_keystrokes("{").await;
2759        cx.shared_state().await.assert_eq(initial_state);
2760
2761        // goes down twice
2762        cx.simulate_shared_keystrokes("2 }").await;
2763        cx.shared_state().await.assert_eq(indoc! {r"abc
2764            def
2765
2766            paragraph
2767            the second
2768            ˇ
2769
2770
2771            third and
2772            final"});
2773
2774        // goes down over multiple blanks
2775        cx.simulate_shared_keystrokes("}").await;
2776        cx.shared_state().await.assert_eq(indoc! {r"abc
2777                def
2778
2779                paragraph
2780                the second
2781
2782
2783
2784                third and
2785                finaˇl"});
2786
2787        // goes up twice
2788        cx.simulate_shared_keystrokes("2 {").await;
2789        cx.shared_state().await.assert_eq(indoc! {r"abc
2790                def
2791                ˇ
2792                paragraph
2793                the second
2794
2795
2796
2797                third and
2798                final"});
2799    }
2800
2801    #[gpui::test]
2802    async fn test_matching(cx: &mut gpui::TestAppContext) {
2803        let mut cx = NeovimBackedTestContext::new(cx).await;
2804
2805        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2806                do(something(with<Types>.and_arrays[0, 2]))
2807            }"})
2808            .await;
2809        cx.simulate_shared_keystrokes("%").await;
2810        cx.shared_state()
2811            .await
2812            .assert_eq(indoc! {r"func (a stringˇ) {
2813                do(something(with<Types>.and_arrays[0, 2]))
2814            }"});
2815
2816        // test it works on the last character of the line
2817        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2818            do(something(with<Types>.and_arrays[0, 2]))
2819            }"})
2820            .await;
2821        cx.simulate_shared_keystrokes("%").await;
2822        cx.shared_state()
2823            .await
2824            .assert_eq(indoc! {r"func (a string) {
2825            do(something(with<Types>.and_arrays[0, 2]))
2826            ˇ}"});
2827
2828        // test it works on immediate nesting
2829        cx.set_shared_state("ˇ{()}").await;
2830        cx.simulate_shared_keystrokes("%").await;
2831        cx.shared_state().await.assert_eq("{()ˇ}");
2832        cx.simulate_shared_keystrokes("%").await;
2833        cx.shared_state().await.assert_eq("ˇ{()}");
2834
2835        // test it works on immediate nesting inside braces
2836        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2837        cx.simulate_shared_keystrokes("%").await;
2838        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2839
2840        // test it jumps to the next paren on a line
2841        cx.set_shared_state("func ˇboop() {\n}").await;
2842        cx.simulate_shared_keystrokes("%").await;
2843        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2844    }
2845
2846    #[gpui::test]
2847    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2848        let mut cx = NeovimBackedTestContext::new(cx).await;
2849
2850        // test it works with curly braces
2851        cx.set_shared_state(indoc! {r"func (a string) {
2852                do(something(with<Types>.anˇd_arrays[0, 2]))
2853            }"})
2854            .await;
2855        cx.simulate_shared_keystrokes("] }").await;
2856        cx.shared_state()
2857            .await
2858            .assert_eq(indoc! {r"func (a string) {
2859                do(something(with<Types>.and_arrays[0, 2]))
2860            ˇ}"});
2861
2862        // test it works with brackets
2863        cx.set_shared_state(indoc! {r"func (a string) {
2864                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2865            }"})
2866            .await;
2867        cx.simulate_shared_keystrokes("] )").await;
2868        cx.shared_state()
2869            .await
2870            .assert_eq(indoc! {r"func (a string) {
2871                do(something(with<Types>.and_arrays[0, 2])ˇ)
2872            }"});
2873
2874        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2875            .await;
2876        cx.simulate_shared_keystrokes("] )").await;
2877        cx.shared_state()
2878            .await
2879            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2880
2881        // test it works on immediate nesting
2882        cx.set_shared_state("{ˇ {}{}}").await;
2883        cx.simulate_shared_keystrokes("] }").await;
2884        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2885        cx.set_shared_state("(ˇ ()())").await;
2886        cx.simulate_shared_keystrokes("] )").await;
2887        cx.shared_state().await.assert_eq("( ()()ˇ)");
2888
2889        // test it works on immediate nesting inside braces
2890        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2891        cx.simulate_shared_keystrokes("] }").await;
2892        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2893        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2894        cx.simulate_shared_keystrokes("] )").await;
2895        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2896    }
2897
2898    #[gpui::test]
2899    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2900        let mut cx = NeovimBackedTestContext::new(cx).await;
2901
2902        // test it works with curly braces
2903        cx.set_shared_state(indoc! {r"func (a string) {
2904                do(something(with<Types>.anˇd_arrays[0, 2]))
2905            }"})
2906            .await;
2907        cx.simulate_shared_keystrokes("[ {").await;
2908        cx.shared_state()
2909            .await
2910            .assert_eq(indoc! {r"func (a string) ˇ{
2911                do(something(with<Types>.and_arrays[0, 2]))
2912            }"});
2913
2914        // test it works with brackets
2915        cx.set_shared_state(indoc! {r"func (a string) {
2916                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2917            }"})
2918            .await;
2919        cx.simulate_shared_keystrokes("[ (").await;
2920        cx.shared_state()
2921            .await
2922            .assert_eq(indoc! {r"func (a string) {
2923                doˇ(something(with<Types>.and_arrays[0, 2]))
2924            }"});
2925
2926        // test it works on immediate nesting
2927        cx.set_shared_state("{{}{} ˇ }").await;
2928        cx.simulate_shared_keystrokes("[ {").await;
2929        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2930        cx.set_shared_state("(()() ˇ )").await;
2931        cx.simulate_shared_keystrokes("[ (").await;
2932        cx.shared_state().await.assert_eq("ˇ(()()  )");
2933
2934        // test it works on immediate nesting inside braces
2935        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2936        cx.simulate_shared_keystrokes("[ {").await;
2937        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2938        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2939        cx.simulate_shared_keystrokes("[ (").await;
2940        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2941    }
2942
2943    #[gpui::test]
2944    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2945        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2946
2947        cx.neovim.exec("set filetype=html").await;
2948
2949        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2950        cx.simulate_shared_keystrokes("%").await;
2951        cx.shared_state()
2952            .await
2953            .assert_eq(indoc! {r"<body><ˇ/body>"});
2954        cx.simulate_shared_keystrokes("%").await;
2955
2956        // test jumping backwards
2957        cx.shared_state()
2958            .await
2959            .assert_eq(indoc! {r"<ˇbody></body>"});
2960
2961        // test self-closing tags
2962        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2963        cx.simulate_shared_keystrokes("%").await;
2964        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2965
2966        // test tag with attributes
2967        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2968            </div>
2969            "})
2970            .await;
2971        cx.simulate_shared_keystrokes("%").await;
2972        cx.shared_state()
2973            .await
2974            .assert_eq(indoc! {r"<div class='test' id='main'>
2975            <ˇ/div>
2976            "});
2977
2978        // test multi-line self-closing tag
2979        cx.set_shared_state(indoc! {r#"<a>
2980            <br
2981                test = "test"
2982            /ˇ>
2983        </a>"#})
2984            .await;
2985        cx.simulate_shared_keystrokes("%").await;
2986        cx.shared_state().await.assert_eq(indoc! {r#"<a>
2987            ˇ<br
2988                test = "test"
2989            />
2990        </a>"#});
2991    }
2992
2993    #[gpui::test]
2994    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2995        let mut cx = NeovimBackedTestContext::new(cx).await;
2996
2997        // f and F
2998        cx.set_shared_state("ˇone two three four").await;
2999        cx.simulate_shared_keystrokes("f o").await;
3000        cx.shared_state().await.assert_eq("one twˇo three four");
3001        cx.simulate_shared_keystrokes(",").await;
3002        cx.shared_state().await.assert_eq("ˇone two three four");
3003        cx.simulate_shared_keystrokes("2 ;").await;
3004        cx.shared_state().await.assert_eq("one two three fˇour");
3005        cx.simulate_shared_keystrokes("shift-f e").await;
3006        cx.shared_state().await.assert_eq("one two threˇe four");
3007        cx.simulate_shared_keystrokes("2 ;").await;
3008        cx.shared_state().await.assert_eq("onˇe two three four");
3009        cx.simulate_shared_keystrokes(",").await;
3010        cx.shared_state().await.assert_eq("one two thrˇee four");
3011
3012        // t and T
3013        cx.set_shared_state("ˇone two three four").await;
3014        cx.simulate_shared_keystrokes("t o").await;
3015        cx.shared_state().await.assert_eq("one tˇwo three four");
3016        cx.simulate_shared_keystrokes(",").await;
3017        cx.shared_state().await.assert_eq("oˇne two three four");
3018        cx.simulate_shared_keystrokes("2 ;").await;
3019        cx.shared_state().await.assert_eq("one two three ˇfour");
3020        cx.simulate_shared_keystrokes("shift-t e").await;
3021        cx.shared_state().await.assert_eq("one two threeˇ four");
3022        cx.simulate_shared_keystrokes("3 ;").await;
3023        cx.shared_state().await.assert_eq("oneˇ two three four");
3024        cx.simulate_shared_keystrokes(",").await;
3025        cx.shared_state().await.assert_eq("one two thˇree four");
3026    }
3027
3028    #[gpui::test]
3029    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3030        let mut cx = NeovimBackedTestContext::new(cx).await;
3031        let initial_state = indoc! {r"something(ˇfoo)"};
3032        cx.set_shared_state(initial_state).await;
3033        cx.simulate_shared_keystrokes("}").await;
3034        cx.shared_state().await.assert_eq("something(fooˇ)");
3035    }
3036
3037    #[gpui::test]
3038    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3039        let mut cx = NeovimBackedTestContext::new(cx).await;
3040        cx.set_shared_state("ˇone\n  two\nthree").await;
3041        cx.simulate_shared_keystrokes("enter").await;
3042        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3043    }
3044
3045    #[gpui::test]
3046    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3047        let mut cx = NeovimBackedTestContext::new(cx).await;
3048        cx.set_shared_state("ˇ one\n two \nthree").await;
3049        cx.simulate_shared_keystrokes("g _").await;
3050        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3051
3052        cx.set_shared_state("ˇ one \n two \nthree").await;
3053        cx.simulate_shared_keystrokes("g _").await;
3054        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3055        cx.simulate_shared_keystrokes("2 g _").await;
3056        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3057    }
3058
3059    #[gpui::test]
3060    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3061        let mut cx = NeovimBackedTestContext::new(cx).await;
3062        let initial_state = indoc! {r"abc
3063          def
3064          paragraph
3065          the second
3066          third ˇand
3067          final"};
3068
3069        cx.set_shared_state(initial_state).await;
3070        cx.simulate_shared_keystrokes("shift-h").await;
3071        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3072          def
3073          paragraph
3074          the second
3075          third and
3076          final"});
3077
3078        // clip point
3079        cx.set_shared_state(indoc! {r"
3080          1 2 3
3081          4 5 6
3082          7 8 ˇ9
3083          "})
3084            .await;
3085        cx.simulate_shared_keystrokes("shift-h").await;
3086        cx.shared_state().await.assert_eq(indoc! {"
3087          1 2 ˇ3
3088          4 5 6
3089          7 8 9
3090          "});
3091
3092        cx.set_shared_state(indoc! {r"
3093          1 2 3
3094          4 5 6
3095          ˇ7 8 9
3096          "})
3097            .await;
3098        cx.simulate_shared_keystrokes("shift-h").await;
3099        cx.shared_state().await.assert_eq(indoc! {"
3100          ˇ1 2 3
3101          4 5 6
3102          7 8 9
3103          "});
3104
3105        cx.set_shared_state(indoc! {r"
3106          1 2 3
3107          4 5 ˇ6
3108          7 8 9"})
3109            .await;
3110        cx.simulate_shared_keystrokes("9 shift-h").await;
3111        cx.shared_state().await.assert_eq(indoc! {"
3112          1 2 3
3113          4 5 6
3114          7 8 ˇ9"});
3115    }
3116
3117    #[gpui::test]
3118    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3119        let mut cx = NeovimBackedTestContext::new(cx).await;
3120        let initial_state = indoc! {r"abˇc
3121          def
3122          paragraph
3123          the second
3124          third and
3125          final"};
3126
3127        cx.set_shared_state(initial_state).await;
3128        cx.simulate_shared_keystrokes("shift-m").await;
3129        cx.shared_state().await.assert_eq(indoc! {r"abc
3130          def
3131          paˇragraph
3132          the second
3133          third and
3134          final"});
3135
3136        cx.set_shared_state(indoc! {r"
3137          1 2 3
3138          4 5 6
3139          7 8 ˇ9
3140          "})
3141            .await;
3142        cx.simulate_shared_keystrokes("shift-m").await;
3143        cx.shared_state().await.assert_eq(indoc! {"
3144          1 2 3
3145          4 5 ˇ6
3146          7 8 9
3147          "});
3148        cx.set_shared_state(indoc! {r"
3149          1 2 3
3150          4 5 6
3151          ˇ7 8 9
3152          "})
3153            .await;
3154        cx.simulate_shared_keystrokes("shift-m").await;
3155        cx.shared_state().await.assert_eq(indoc! {"
3156          1 2 3
3157          ˇ4 5 6
3158          7 8 9
3159          "});
3160        cx.set_shared_state(indoc! {r"
3161          ˇ1 2 3
3162          4 5 6
3163          7 8 9
3164          "})
3165            .await;
3166        cx.simulate_shared_keystrokes("shift-m").await;
3167        cx.shared_state().await.assert_eq(indoc! {"
3168          1 2 3
3169          ˇ4 5 6
3170          7 8 9
3171          "});
3172        cx.set_shared_state(indoc! {r"
3173          1 2 3
3174          ˇ4 5 6
3175          7 8 9
3176          "})
3177            .await;
3178        cx.simulate_shared_keystrokes("shift-m").await;
3179        cx.shared_state().await.assert_eq(indoc! {"
3180          1 2 3
3181          ˇ4 5 6
3182          7 8 9
3183          "});
3184        cx.set_shared_state(indoc! {r"
3185          1 2 3
3186          4 5 ˇ6
3187          7 8 9
3188          "})
3189            .await;
3190        cx.simulate_shared_keystrokes("shift-m").await;
3191        cx.shared_state().await.assert_eq(indoc! {"
3192          1 2 3
3193          4 5 ˇ6
3194          7 8 9
3195          "});
3196    }
3197
3198    #[gpui::test]
3199    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3200        let mut cx = NeovimBackedTestContext::new(cx).await;
3201        let initial_state = indoc! {r"abc
3202          deˇf
3203          paragraph
3204          the second
3205          third and
3206          final"};
3207
3208        cx.set_shared_state(initial_state).await;
3209        cx.simulate_shared_keystrokes("shift-l").await;
3210        cx.shared_state().await.assert_eq(indoc! {r"abc
3211          def
3212          paragraph
3213          the second
3214          third and
3215          fiˇnal"});
3216
3217        cx.set_shared_state(indoc! {r"
3218          1 2 3
3219          4 5 ˇ6
3220          7 8 9
3221          "})
3222            .await;
3223        cx.simulate_shared_keystrokes("shift-l").await;
3224        cx.shared_state().await.assert_eq(indoc! {"
3225          1 2 3
3226          4 5 6
3227          7 8 9
3228          ˇ"});
3229
3230        cx.set_shared_state(indoc! {r"
3231          1 2 3
3232          ˇ4 5 6
3233          7 8 9
3234          "})
3235            .await;
3236        cx.simulate_shared_keystrokes("shift-l").await;
3237        cx.shared_state().await.assert_eq(indoc! {"
3238          1 2 3
3239          4 5 6
3240          7 8 9
3241          ˇ"});
3242
3243        cx.set_shared_state(indoc! {r"
3244          1 2 ˇ3
3245          4 5 6
3246          7 8 9
3247          "})
3248            .await;
3249        cx.simulate_shared_keystrokes("shift-l").await;
3250        cx.shared_state().await.assert_eq(indoc! {"
3251          1 2 3
3252          4 5 6
3253          7 8 9
3254          ˇ"});
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("9 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
3283    #[gpui::test]
3284    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3285        let mut cx = NeovimBackedTestContext::new(cx).await;
3286        cx.set_shared_state(indoc! {r"
3287        456 5ˇ67 678
3288        "})
3289            .await;
3290        cx.simulate_shared_keystrokes("g e").await;
3291        cx.shared_state().await.assert_eq(indoc! {"
3292        45ˇ6 567 678
3293        "});
3294
3295        // Test times
3296        cx.set_shared_state(indoc! {r"
3297        123 234 345
3298        456 5ˇ67 678
3299        "})
3300            .await;
3301        cx.simulate_shared_keystrokes("4 g e").await;
3302        cx.shared_state().await.assert_eq(indoc! {"
3303        12ˇ3 234 345
3304        456 567 678
3305        "});
3306
3307        // With punctuation
3308        cx.set_shared_state(indoc! {r"
3309        123 234 345
3310        4;5.6 5ˇ67 678
3311        789 890 901
3312        "})
3313            .await;
3314        cx.simulate_shared_keystrokes("g e").await;
3315        cx.shared_state().await.assert_eq(indoc! {"
3316          123 234 345
3317          4;5.ˇ6 567 678
3318          789 890 901
3319        "});
3320
3321        // With punctuation and count
3322        cx.set_shared_state(indoc! {r"
3323        123 234 345
3324        4;5.6 5ˇ67 678
3325        789 890 901
3326        "})
3327            .await;
3328        cx.simulate_shared_keystrokes("5 g e").await;
3329        cx.shared_state().await.assert_eq(indoc! {"
3330          123 234 345
3331          ˇ4;5.6 567 678
3332          789 890 901
3333        "});
3334
3335        // newlines
3336        cx.set_shared_state(indoc! {r"
3337        123 234 345
3338
3339        78ˇ9 890 901
3340        "})
3341            .await;
3342        cx.simulate_shared_keystrokes("g e").await;
3343        cx.shared_state().await.assert_eq(indoc! {"
3344          123 234 345
3345          ˇ
3346          789 890 901
3347        "});
3348        cx.simulate_shared_keystrokes("g e").await;
3349        cx.shared_state().await.assert_eq(indoc! {"
3350          123 234 34ˇ5
3351
3352          789 890 901
3353        "});
3354
3355        // With punctuation
3356        cx.set_shared_state(indoc! {r"
3357        123 234 345
3358        4;5.ˇ6 567 678
3359        789 890 901
3360        "})
3361            .await;
3362        cx.simulate_shared_keystrokes("g shift-e").await;
3363        cx.shared_state().await.assert_eq(indoc! {"
3364          123 234 34ˇ5
3365          4;5.6 567 678
3366          789 890 901
3367        "});
3368    }
3369
3370    #[gpui::test]
3371    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3372        let mut cx = NeovimBackedTestContext::new(cx).await;
3373
3374        cx.set_shared_state(indoc! {"
3375            fn aˇ() {
3376              return
3377            }
3378        "})
3379            .await;
3380        cx.simulate_shared_keystrokes("v $ %").await;
3381        cx.shared_state().await.assert_eq(indoc! {"
3382            fn a«() {
3383              return
3384            }ˇ»
3385        "});
3386    }
3387
3388    #[gpui::test]
3389    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3390        let mut cx = VimTestContext::new(cx, true).await;
3391
3392        cx.set_state(
3393            indoc! {"
3394                struct Foo {
3395                ˇ
3396                }
3397            "},
3398            Mode::Normal,
3399        );
3400
3401        cx.update_editor(|editor, _window, cx| {
3402            let range = editor.selections.newest_anchor().range();
3403            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3404            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3405            editor.splice_inlays(&[], vec![inlay], cx);
3406        });
3407
3408        cx.simulate_keystrokes("j");
3409        cx.assert_state(
3410            indoc! {"
3411                struct Foo {
3412
3413                ˇ}
3414            "},
3415            Mode::Normal,
3416        );
3417    }
3418
3419    #[gpui::test]
3420    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3421        let mut cx = VimTestContext::new(cx, true).await;
3422
3423        cx.set_state(
3424            indoc! {"
3425            ˇstruct Foo {
3426
3427            }
3428        "},
3429            Mode::Normal,
3430        );
3431        cx.update_editor(|editor, _window, cx| {
3432            let snapshot = editor.buffer().read(cx).snapshot(cx);
3433            let end_of_line =
3434                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3435            let inlay_text = " hint";
3436            let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3437            editor.splice_inlays(&[], vec![inlay], cx);
3438        });
3439        cx.simulate_keystrokes("$");
3440        cx.assert_state(
3441            indoc! {"
3442            struct Foo ˇ{
3443
3444            }
3445        "},
3446            Mode::Normal,
3447        );
3448    }
3449}