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(rename_all = "camelCase")]
 145struct NextWordStart {
 146    #[serde(default)]
 147    ignore_punctuation: bool,
 148}
 149
 150#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 151#[serde(rename_all = "camelCase")]
 152struct NextWordEnd {
 153    #[serde(default)]
 154    ignore_punctuation: bool,
 155}
 156
 157#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 158#[serde(rename_all = "camelCase")]
 159struct PreviousWordStart {
 160    #[serde(default)]
 161    ignore_punctuation: bool,
 162}
 163
 164#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 165#[serde(rename_all = "camelCase")]
 166struct PreviousWordEnd {
 167    #[serde(default)]
 168    ignore_punctuation: bool,
 169}
 170
 171#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 172#[serde(rename_all = "camelCase")]
 173pub(crate) struct NextSubwordStart {
 174    #[serde(default)]
 175    pub(crate) ignore_punctuation: bool,
 176}
 177
 178#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 179#[serde(rename_all = "camelCase")]
 180pub(crate) struct NextSubwordEnd {
 181    #[serde(default)]
 182    pub(crate) ignore_punctuation: bool,
 183}
 184
 185#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 186#[serde(rename_all = "camelCase")]
 187pub(crate) struct PreviousSubwordStart {
 188    #[serde(default)]
 189    pub(crate) ignore_punctuation: bool,
 190}
 191
 192#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 193#[serde(rename_all = "camelCase")]
 194pub(crate) struct PreviousSubwordEnd {
 195    #[serde(default)]
 196    pub(crate) ignore_punctuation: bool,
 197}
 198
 199#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 200#[serde(rename_all = "camelCase")]
 201pub(crate) struct Up {
 202    #[serde(default)]
 203    pub(crate) display_lines: bool,
 204}
 205
 206#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 207#[serde(rename_all = "camelCase")]
 208pub(crate) struct Down {
 209    #[serde(default)]
 210    pub(crate) display_lines: bool,
 211}
 212
 213#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 214#[serde(rename_all = "camelCase")]
 215struct FirstNonWhitespace {
 216    #[serde(default)]
 217    display_lines: bool,
 218}
 219
 220#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 221#[serde(rename_all = "camelCase")]
 222struct EndOfLine {
 223    #[serde(default)]
 224    display_lines: bool,
 225}
 226
 227#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 228#[serde(rename_all = "camelCase")]
 229pub struct StartOfLine {
 230    #[serde(default)]
 231    pub(crate) display_lines: bool,
 232}
 233
 234#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 235#[serde(rename_all = "camelCase")]
 236struct UnmatchedForward {
 237    #[serde(default)]
 238    char: char,
 239}
 240
 241#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 242#[serde(rename_all = "camelCase")]
 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        if point == new_point {
1679            break;
1680        }
1681        point = new_point;
1682    }
1683    point
1684}
1685
1686fn previous_subword_start(
1687    map: &DisplaySnapshot,
1688    mut point: DisplayPoint,
1689    ignore_punctuation: bool,
1690    times: usize,
1691) -> DisplayPoint {
1692    let classifier = map
1693        .buffer_snapshot
1694        .char_classifier_at(point.to_point(map))
1695        .ignore_punctuation(ignore_punctuation);
1696    for _ in 0..times {
1697        let mut crossed_newline = false;
1698        // This works even though find_preceding_boundary is called for every character in the line containing
1699        // cursor because the newline is checked only once.
1700        let new_point = movement::find_preceding_boundary_display_point(
1701            map,
1702            point,
1703            FindRange::MultiLine,
1704            |left, right| {
1705                let left_kind = classifier.kind(left);
1706                let right_kind = classifier.kind(right);
1707                let at_newline = right == '\n';
1708
1709                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1710                let is_subword_start =
1711                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1712
1713                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1714                    || at_newline && crossed_newline
1715                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1716
1717                crossed_newline |= at_newline;
1718
1719                found
1720            },
1721        );
1722        if point == new_point {
1723            break;
1724        }
1725        point = new_point;
1726    }
1727    point
1728}
1729
1730fn previous_subword_end(
1731    map: &DisplaySnapshot,
1732    point: DisplayPoint,
1733    ignore_punctuation: bool,
1734    times: usize,
1735) -> DisplayPoint {
1736    let classifier = map
1737        .buffer_snapshot
1738        .char_classifier_at(point.to_point(map))
1739        .ignore_punctuation(ignore_punctuation);
1740    let mut point = point.to_point(map);
1741
1742    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1743        point.column += 1;
1744    }
1745    for _ in 0..times {
1746        let new_point = movement::find_preceding_boundary_point(
1747            &map.buffer_snapshot,
1748            point,
1749            FindRange::MultiLine,
1750            |left, right| {
1751                let left_kind = classifier.kind(left);
1752                let right_kind = classifier.kind(right);
1753
1754                let is_subword_end =
1755                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1756
1757                if is_subword_end {
1758                    return true;
1759                }
1760
1761                match (left_kind, right_kind) {
1762                    (CharKind::Word, CharKind::Whitespace)
1763                    | (CharKind::Word, CharKind::Punctuation) => true,
1764                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1765                    _ => false,
1766                }
1767            },
1768        );
1769        if new_point == point {
1770            break;
1771        }
1772        point = new_point;
1773    }
1774    movement::saturating_left(map, point.to_display_point(map))
1775}
1776
1777pub(crate) fn first_non_whitespace(
1778    map: &DisplaySnapshot,
1779    display_lines: bool,
1780    from: DisplayPoint,
1781) -> DisplayPoint {
1782    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1783    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1784    for (ch, offset) in map.buffer_chars_at(start_offset) {
1785        if ch == '\n' {
1786            return from;
1787        }
1788
1789        start_offset = offset;
1790
1791        if classifier.kind(ch) != CharKind::Whitespace {
1792            break;
1793        }
1794    }
1795
1796    start_offset.to_display_point(map)
1797}
1798
1799pub(crate) fn last_non_whitespace(
1800    map: &DisplaySnapshot,
1801    from: DisplayPoint,
1802    count: usize,
1803) -> DisplayPoint {
1804    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1805    let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1806
1807    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1808    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1809        if classifier.kind(ch) != CharKind::Whitespace {
1810            return end_of_line.to_display_point(map);
1811        }
1812    }
1813
1814    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1815        if ch == '\n' {
1816            break;
1817        }
1818        end_of_line = offset;
1819        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1820            break;
1821        }
1822    }
1823
1824    end_of_line.to_display_point(map)
1825}
1826
1827pub(crate) fn start_of_line(
1828    map: &DisplaySnapshot,
1829    display_lines: bool,
1830    point: DisplayPoint,
1831) -> DisplayPoint {
1832    if display_lines {
1833        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1834    } else {
1835        map.prev_line_boundary(point.to_point(map)).1
1836    }
1837}
1838
1839pub(crate) fn end_of_line(
1840    map: &DisplaySnapshot,
1841    display_lines: bool,
1842    mut point: DisplayPoint,
1843    times: usize,
1844) -> DisplayPoint {
1845    if times > 1 {
1846        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1847    }
1848    if display_lines {
1849        map.clip_point(
1850            DisplayPoint::new(point.row(), map.line_len(point.row())),
1851            Bias::Left,
1852        )
1853    } else {
1854        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1855    }
1856}
1857
1858fn sentence_backwards(
1859    map: &DisplaySnapshot,
1860    point: DisplayPoint,
1861    mut times: usize,
1862) -> DisplayPoint {
1863    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1864    let mut chars = map.reverse_buffer_chars_at(start).peekable();
1865
1866    let mut was_newline = map
1867        .buffer_chars_at(start)
1868        .next()
1869        .is_some_and(|(c, _)| c == '\n');
1870
1871    while let Some((ch, offset)) = chars.next() {
1872        let start_of_next_sentence = if was_newline && ch == '\n' {
1873            Some(offset + ch.len_utf8())
1874        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1875            Some(next_non_blank(map, offset + ch.len_utf8()))
1876        } else if ch == '.' || ch == '?' || ch == '!' {
1877            start_of_next_sentence(map, offset + ch.len_utf8())
1878        } else {
1879            None
1880        };
1881
1882        if let Some(start_of_next_sentence) = start_of_next_sentence {
1883            if start_of_next_sentence < start {
1884                times = times.saturating_sub(1);
1885            }
1886            if times == 0 || offset == 0 {
1887                return map.clip_point(
1888                    start_of_next_sentence
1889                        .to_offset(&map.buffer_snapshot)
1890                        .to_display_point(map),
1891                    Bias::Left,
1892                );
1893            }
1894        }
1895        if was_newline {
1896            start = offset;
1897        }
1898        was_newline = ch == '\n';
1899    }
1900
1901    DisplayPoint::zero()
1902}
1903
1904fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1905    let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1906    let mut chars = map.buffer_chars_at(start).peekable();
1907
1908    let mut was_newline = map
1909        .reverse_buffer_chars_at(start)
1910        .next()
1911        .is_some_and(|(c, _)| c == '\n')
1912        && chars.peek().is_some_and(|(c, _)| *c == '\n');
1913
1914    while let Some((ch, offset)) = chars.next() {
1915        if was_newline && ch == '\n' {
1916            continue;
1917        }
1918        let start_of_next_sentence = if was_newline {
1919            Some(next_non_blank(map, offset))
1920        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1921            Some(next_non_blank(map, offset + ch.len_utf8()))
1922        } else if ch == '.' || ch == '?' || ch == '!' {
1923            start_of_next_sentence(map, offset + ch.len_utf8())
1924        } else {
1925            None
1926        };
1927
1928        if let Some(start_of_next_sentence) = start_of_next_sentence {
1929            times = times.saturating_sub(1);
1930            if times == 0 {
1931                return map.clip_point(
1932                    start_of_next_sentence
1933                        .to_offset(&map.buffer_snapshot)
1934                        .to_display_point(map),
1935                    Bias::Right,
1936                );
1937            }
1938        }
1939
1940        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1941    }
1942
1943    map.max_point()
1944}
1945
1946fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1947    for (c, o) in map.buffer_chars_at(start) {
1948        if c == '\n' || !c.is_whitespace() {
1949            return o;
1950        }
1951    }
1952
1953    map.buffer_snapshot.len()
1954}
1955
1956// given the offset after a ., !, or ? find the start of the next sentence.
1957// if this is not a sentence boundary, returns None.
1958fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1959    let chars = map.buffer_chars_at(end_of_sentence);
1960    let mut seen_space = false;
1961
1962    for (char, offset) in chars {
1963        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1964            continue;
1965        }
1966
1967        if char == '\n' && seen_space {
1968            return Some(offset);
1969        } else if char.is_whitespace() {
1970            seen_space = true;
1971        } else if seen_space {
1972            return Some(offset);
1973        } else {
1974            return None;
1975        }
1976    }
1977
1978    Some(map.buffer_snapshot.len())
1979}
1980
1981fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
1982    let point = map.display_point_to_point(display_point, Bias::Left);
1983    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
1984        return display_point;
1985    };
1986    let offset = excerpt.buffer().point_to_offset(
1987        excerpt
1988            .buffer()
1989            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
1990    );
1991    let buffer_range = excerpt.buffer_range();
1992    if offset >= buffer_range.start && offset <= buffer_range.end {
1993        let point = map
1994            .buffer_snapshot
1995            .offset_to_point(excerpt.map_offset_from_buffer(offset));
1996        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
1997    }
1998    let mut last_position = None;
1999    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2000        let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2001            ..language::ToOffset::to_offset(&range.context.end, &buffer);
2002        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2003            let text_anchor = buffer.anchor_after(offset);
2004            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2005            return anchor.to_display_point(map);
2006        } else if offset <= excerpt_range.start {
2007            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2008            return anchor.to_display_point(map);
2009        } else {
2010            last_position = Some(Anchor::in_buffer(
2011                excerpt,
2012                buffer.remote_id(),
2013                range.context.end,
2014            ));
2015        }
2016    }
2017
2018    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2019    last_point.column = point.column;
2020
2021    map.clip_point(
2022        map.point_to_display_point(
2023            map.buffer_snapshot.clip_point(point, Bias::Left),
2024            Bias::Left,
2025        ),
2026        Bias::Left,
2027    )
2028}
2029
2030fn start_of_document(
2031    map: &DisplaySnapshot,
2032    display_point: DisplayPoint,
2033    maybe_times: Option<usize>,
2034) -> DisplayPoint {
2035    if let Some(times) = maybe_times {
2036        return go_to_line(map, display_point, times);
2037    }
2038
2039    let point = map.display_point_to_point(display_point, Bias::Left);
2040    let mut first_point = Point::zero();
2041    first_point.column = point.column;
2042
2043    map.clip_point(
2044        map.point_to_display_point(
2045            map.buffer_snapshot.clip_point(first_point, Bias::Left),
2046            Bias::Left,
2047        ),
2048        Bias::Left,
2049    )
2050}
2051
2052fn end_of_document(
2053    map: &DisplaySnapshot,
2054    display_point: DisplayPoint,
2055    maybe_times: Option<usize>,
2056) -> DisplayPoint {
2057    if let Some(times) = maybe_times {
2058        return go_to_line(map, display_point, times);
2059    };
2060    let point = map.display_point_to_point(display_point, Bias::Left);
2061    let mut last_point = map.buffer_snapshot.max_point();
2062    last_point.column = point.column;
2063
2064    map.clip_point(
2065        map.point_to_display_point(
2066            map.buffer_snapshot.clip_point(last_point, Bias::Left),
2067            Bias::Left,
2068        ),
2069        Bias::Left,
2070    )
2071}
2072
2073fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2074    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2075    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2076
2077    if head > outer.start && head < inner.start {
2078        let mut offset = inner.end.to_offset(map, Bias::Left);
2079        for c in map.buffer_snapshot.chars_at(offset) {
2080            if c == '/' || c == '\n' || c == '>' {
2081                return Some(offset.to_display_point(map));
2082            }
2083            offset += c.len_utf8();
2084        }
2085    } else {
2086        let mut offset = outer.start.to_offset(map, Bias::Left);
2087        for c in map.buffer_snapshot.chars_at(offset) {
2088            offset += c.len_utf8();
2089            if c == '<' || c == '\n' {
2090                return Some(offset.to_display_point(map));
2091            }
2092        }
2093    }
2094
2095    return None;
2096}
2097
2098fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2099    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2100    let display_point = map.clip_at_line_end(display_point);
2101    let point = display_point.to_point(map);
2102    let offset = point.to_offset(&map.buffer_snapshot);
2103
2104    // Ensure the range is contained by the current line.
2105    let mut line_end = map.next_line_boundary(point).0;
2106    if line_end == point {
2107        line_end = map.max_point().to_point(map);
2108    }
2109
2110    let line_range = map.prev_line_boundary(point).0..line_end;
2111    let visible_line_range =
2112        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2113    let ranges = map
2114        .buffer_snapshot
2115        .bracket_ranges(visible_line_range.clone());
2116    if let Some(ranges) = ranges {
2117        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2118            ..line_range.end.to_offset(&map.buffer_snapshot);
2119        let mut closest_pair_destination = None;
2120        let mut closest_distance = usize::MAX;
2121
2122        for (open_range, close_range) in ranges {
2123            if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2124                if offset > open_range.start && offset < close_range.start {
2125                    let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2126                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2127                        return display_point;
2128                    }
2129                    if let Some(tag) = matching_tag(map, display_point) {
2130                        return tag;
2131                    }
2132                } else if close_range.contains(&offset) {
2133                    return open_range.start.to_display_point(map);
2134                } else if open_range.contains(&offset) {
2135                    return (close_range.end - 1).to_display_point(map);
2136                }
2137            }
2138
2139            if (open_range.contains(&offset) || open_range.start >= offset)
2140                && line_range.contains(&open_range.start)
2141            {
2142                let distance = open_range.start.saturating_sub(offset);
2143                if distance < closest_distance {
2144                    closest_pair_destination = Some(close_range.start);
2145                    closest_distance = distance;
2146                    continue;
2147                }
2148            }
2149
2150            if (close_range.contains(&offset) || close_range.start >= offset)
2151                && line_range.contains(&close_range.start)
2152            {
2153                let distance = close_range.start.saturating_sub(offset);
2154                if distance < closest_distance {
2155                    closest_pair_destination = Some(open_range.start);
2156                    closest_distance = distance;
2157                    continue;
2158                }
2159            }
2160
2161            continue;
2162        }
2163
2164        closest_pair_destination
2165            .map(|destination| destination.to_display_point(map))
2166            .unwrap_or(display_point)
2167    } else {
2168        display_point
2169    }
2170}
2171
2172fn unmatched_forward(
2173    map: &DisplaySnapshot,
2174    mut display_point: DisplayPoint,
2175    char: char,
2176    times: usize,
2177) -> DisplayPoint {
2178    for _ in 0..times {
2179        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2180        let point = display_point.to_point(map);
2181        let offset = point.to_offset(&map.buffer_snapshot);
2182
2183        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2184        let Some(ranges) = ranges else { break };
2185        let mut closest_closing_destination = None;
2186        let mut closest_distance = usize::MAX;
2187
2188        for (_, close_range) in ranges {
2189            if close_range.start > offset {
2190                let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2191                if Some(char) == chars.next() {
2192                    let distance = close_range.start - offset;
2193                    if distance < closest_distance {
2194                        closest_closing_destination = Some(close_range.start);
2195                        closest_distance = distance;
2196                        continue;
2197                    }
2198                }
2199            }
2200        }
2201
2202        let new_point = closest_closing_destination
2203            .map(|destination| destination.to_display_point(map))
2204            .unwrap_or(display_point);
2205        if new_point == display_point {
2206            break;
2207        }
2208        display_point = new_point;
2209    }
2210    return display_point;
2211}
2212
2213fn unmatched_backward(
2214    map: &DisplaySnapshot,
2215    mut display_point: DisplayPoint,
2216    char: char,
2217    times: usize,
2218) -> DisplayPoint {
2219    for _ in 0..times {
2220        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2221        let point = display_point.to_point(map);
2222        let offset = point.to_offset(&map.buffer_snapshot);
2223
2224        let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2225        let Some(ranges) = ranges else {
2226            break;
2227        };
2228
2229        let mut closest_starting_destination = None;
2230        let mut closest_distance = usize::MAX;
2231
2232        for (start_range, _) in ranges {
2233            if start_range.start < offset {
2234                let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2235                if Some(char) == chars.next() {
2236                    let distance = offset - start_range.start;
2237                    if distance < closest_distance {
2238                        closest_starting_destination = Some(start_range.start);
2239                        closest_distance = distance;
2240                        continue;
2241                    }
2242                }
2243            }
2244        }
2245
2246        let new_point = closest_starting_destination
2247            .map(|destination| destination.to_display_point(map))
2248            .unwrap_or(display_point);
2249        if new_point == display_point {
2250            break;
2251        } else {
2252            display_point = new_point;
2253        }
2254    }
2255    display_point
2256}
2257
2258fn find_forward(
2259    map: &DisplaySnapshot,
2260    from: DisplayPoint,
2261    before: bool,
2262    target: char,
2263    times: usize,
2264    mode: FindRange,
2265    smartcase: bool,
2266) -> Option<DisplayPoint> {
2267    let mut to = from;
2268    let mut found = false;
2269
2270    for _ in 0..times {
2271        found = false;
2272        let new_to = find_boundary(map, to, mode, |_, right| {
2273            found = is_character_match(target, right, smartcase);
2274            found
2275        });
2276        if to == new_to {
2277            break;
2278        }
2279        to = new_to;
2280    }
2281
2282    if found {
2283        if before && to.column() > 0 {
2284            *to.column_mut() -= 1;
2285            Some(map.clip_point(to, Bias::Left))
2286        } else {
2287            Some(to)
2288        }
2289    } else {
2290        None
2291    }
2292}
2293
2294fn find_backward(
2295    map: &DisplaySnapshot,
2296    from: DisplayPoint,
2297    after: bool,
2298    target: char,
2299    times: usize,
2300    mode: FindRange,
2301    smartcase: bool,
2302) -> DisplayPoint {
2303    let mut to = from;
2304
2305    for _ in 0..times {
2306        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2307            is_character_match(target, right, smartcase)
2308        });
2309        if to == new_to {
2310            break;
2311        }
2312        to = new_to;
2313    }
2314
2315    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2316    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2317        if after {
2318            *to.column_mut() += 1;
2319            map.clip_point(to, Bias::Right)
2320        } else {
2321            to
2322        }
2323    } else {
2324        from
2325    }
2326}
2327
2328fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2329    if smartcase {
2330        if target.is_uppercase() {
2331            target == other
2332        } else {
2333            target == other.to_ascii_lowercase()
2334        }
2335    } else {
2336        target == other
2337    }
2338}
2339
2340fn sneak(
2341    map: &DisplaySnapshot,
2342    from: DisplayPoint,
2343    first_target: char,
2344    second_target: char,
2345    times: usize,
2346    smartcase: bool,
2347) -> Option<DisplayPoint> {
2348    let mut to = from;
2349    let mut found = false;
2350
2351    for _ in 0..times {
2352        found = false;
2353        let new_to = find_boundary(
2354            map,
2355            movement::right(map, to),
2356            FindRange::MultiLine,
2357            |left, right| {
2358                found = is_character_match(first_target, left, smartcase)
2359                    && is_character_match(second_target, right, smartcase);
2360                found
2361            },
2362        );
2363        if to == new_to {
2364            break;
2365        }
2366        to = new_to;
2367    }
2368
2369    if found {
2370        Some(movement::left(map, to))
2371    } else {
2372        None
2373    }
2374}
2375
2376fn sneak_backward(
2377    map: &DisplaySnapshot,
2378    from: DisplayPoint,
2379    first_target: char,
2380    second_target: char,
2381    times: usize,
2382    smartcase: bool,
2383) -> Option<DisplayPoint> {
2384    let mut to = from;
2385    let mut found = false;
2386
2387    for _ in 0..times {
2388        found = false;
2389        let new_to =
2390            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2391                found = is_character_match(first_target, left, smartcase)
2392                    && is_character_match(second_target, right, smartcase);
2393                found
2394            });
2395        if to == new_to {
2396            break;
2397        }
2398        to = new_to;
2399    }
2400
2401    if found {
2402        Some(movement::left(map, to))
2403    } else {
2404        None
2405    }
2406}
2407
2408fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2409    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2410    first_non_whitespace(map, false, correct_line)
2411}
2412
2413fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2414    let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2415    first_non_whitespace(map, false, correct_line)
2416}
2417
2418fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2419    let correct_line = start_of_relative_buffer_row(map, point, 0);
2420    right(map, correct_line, times.saturating_sub(1))
2421}
2422
2423pub(crate) fn next_line_end(
2424    map: &DisplaySnapshot,
2425    mut point: DisplayPoint,
2426    times: usize,
2427) -> DisplayPoint {
2428    if times > 1 {
2429        point = start_of_relative_buffer_row(map, point, times as isize - 1);
2430    }
2431    end_of_line(map, false, point, 1)
2432}
2433
2434fn window_top(
2435    map: &DisplaySnapshot,
2436    point: DisplayPoint,
2437    text_layout_details: &TextLayoutDetails,
2438    mut times: usize,
2439) -> (DisplayPoint, SelectionGoal) {
2440    let first_visible_line = text_layout_details
2441        .scroll_anchor
2442        .anchor
2443        .to_display_point(map);
2444
2445    if first_visible_line.row() != DisplayRow(0)
2446        && text_layout_details.vertical_scroll_margin as usize > times
2447    {
2448        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2449    }
2450
2451    if let Some(visible_rows) = text_layout_details.visible_rows {
2452        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2453        let new_row = (first_visible_line.row().0 + (times as u32))
2454            .min(bottom_row)
2455            .min(map.max_point().row().0);
2456        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2457
2458        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2459        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2460    } else {
2461        let new_row =
2462            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2463        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2464
2465        let new_point = DisplayPoint::new(new_row, new_col);
2466        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2467    }
2468}
2469
2470fn window_middle(
2471    map: &DisplaySnapshot,
2472    point: DisplayPoint,
2473    text_layout_details: &TextLayoutDetails,
2474) -> (DisplayPoint, SelectionGoal) {
2475    if let Some(visible_rows) = text_layout_details.visible_rows {
2476        let first_visible_line = text_layout_details
2477            .scroll_anchor
2478            .anchor
2479            .to_display_point(map);
2480
2481        let max_visible_rows =
2482            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2483
2484        let new_row =
2485            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2486        let new_row = DisplayRow(new_row);
2487        let new_col = point.column().min(map.line_len(new_row));
2488        let new_point = DisplayPoint::new(new_row, new_col);
2489        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2490    } else {
2491        (point, SelectionGoal::None)
2492    }
2493}
2494
2495fn window_bottom(
2496    map: &DisplaySnapshot,
2497    point: DisplayPoint,
2498    text_layout_details: &TextLayoutDetails,
2499    mut times: usize,
2500) -> (DisplayPoint, SelectionGoal) {
2501    if let Some(visible_rows) = text_layout_details.visible_rows {
2502        let first_visible_line = text_layout_details
2503            .scroll_anchor
2504            .anchor
2505            .to_display_point(map);
2506        let bottom_row = first_visible_line.row().0
2507            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2508        if bottom_row < map.max_point().row().0
2509            && text_layout_details.vertical_scroll_margin as usize > times
2510        {
2511            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2512        }
2513        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2514        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2515        {
2516            first_visible_line.row()
2517        } else {
2518            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2519        };
2520        let new_col = point.column().min(map.line_len(new_row));
2521        let new_point = DisplayPoint::new(new_row, new_col);
2522        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2523    } else {
2524        (point, SelectionGoal::None)
2525    }
2526}
2527
2528fn method_motion(
2529    map: &DisplaySnapshot,
2530    mut display_point: DisplayPoint,
2531    times: usize,
2532    direction: Direction,
2533    is_start: bool,
2534) -> DisplayPoint {
2535    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2536        return display_point;
2537    };
2538
2539    for _ in 0..times {
2540        let point = map.display_point_to_point(display_point, Bias::Left);
2541        let offset = point.to_offset(&map.buffer_snapshot);
2542        let range = if direction == Direction::Prev {
2543            0..offset
2544        } else {
2545            offset..buffer.len()
2546        };
2547
2548        let possibilities = buffer
2549            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2550            .filter_map(|(range, object)| {
2551                if !matches!(object, language::TextObject::AroundFunction) {
2552                    return None;
2553                }
2554
2555                let relevant = if is_start { range.start } else { range.end };
2556                if direction == Direction::Prev && relevant < offset {
2557                    Some(relevant)
2558                } else if direction == Direction::Next && relevant > offset + 1 {
2559                    Some(relevant)
2560                } else {
2561                    None
2562                }
2563            });
2564
2565        let dest = if direction == Direction::Prev {
2566            possibilities.max().unwrap_or(offset)
2567        } else {
2568            possibilities.min().unwrap_or(offset)
2569        };
2570        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2571        if new_point == display_point {
2572            break;
2573        }
2574        display_point = new_point;
2575    }
2576    display_point
2577}
2578
2579fn comment_motion(
2580    map: &DisplaySnapshot,
2581    mut display_point: DisplayPoint,
2582    times: usize,
2583    direction: Direction,
2584) -> DisplayPoint {
2585    let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2586        return display_point;
2587    };
2588
2589    for _ in 0..times {
2590        let point = map.display_point_to_point(display_point, Bias::Left);
2591        let offset = point.to_offset(&map.buffer_snapshot);
2592        let range = if direction == Direction::Prev {
2593            0..offset
2594        } else {
2595            offset..buffer.len()
2596        };
2597
2598        let possibilities = buffer
2599            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2600            .filter_map(|(range, object)| {
2601                if !matches!(object, language::TextObject::AroundComment) {
2602                    return None;
2603                }
2604
2605                let relevant = if direction == Direction::Prev {
2606                    range.start
2607                } else {
2608                    range.end
2609                };
2610                if direction == Direction::Prev && relevant < offset {
2611                    Some(relevant)
2612                } else if direction == Direction::Next && relevant > offset + 1 {
2613                    Some(relevant)
2614                } else {
2615                    None
2616                }
2617            });
2618
2619        let dest = if direction == Direction::Prev {
2620            possibilities.max().unwrap_or(offset)
2621        } else {
2622            possibilities.min().unwrap_or(offset)
2623        };
2624        let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2625        if new_point == display_point {
2626            break;
2627        }
2628        display_point = new_point;
2629    }
2630
2631    display_point
2632}
2633
2634fn section_motion(
2635    map: &DisplaySnapshot,
2636    mut display_point: DisplayPoint,
2637    times: usize,
2638    direction: Direction,
2639    is_start: bool,
2640) -> DisplayPoint {
2641    if map.buffer_snapshot.as_singleton().is_some() {
2642        for _ in 0..times {
2643            let offset = map
2644                .display_point_to_point(display_point, Bias::Left)
2645                .to_offset(&map.buffer_snapshot);
2646            let range = if direction == Direction::Prev {
2647                0..offset
2648            } else {
2649                offset..map.buffer_snapshot.len()
2650            };
2651
2652            // we set a max start depth here because we want a section to only be "top level"
2653            // similar to vim's default of '{' in the first column.
2654            // (and without it, ]] at the start of editor.rs is -very- slow)
2655            let mut possibilities = map
2656                .buffer_snapshot
2657                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2658                .filter(|(_, object)| {
2659                    matches!(
2660                        object,
2661                        language::TextObject::AroundClass | language::TextObject::AroundFunction
2662                    )
2663                })
2664                .collect::<Vec<_>>();
2665            possibilities.sort_by_key(|(range_a, _)| range_a.start);
2666            let mut prev_end = None;
2667            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2668                if t == language::TextObject::AroundFunction
2669                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
2670                {
2671                    return None;
2672                }
2673                prev_end = Some(range.end);
2674
2675                let relevant = if is_start { range.start } else { range.end };
2676                if direction == Direction::Prev && relevant < offset {
2677                    Some(relevant)
2678                } else if direction == Direction::Next && relevant > offset + 1 {
2679                    Some(relevant)
2680                } else {
2681                    None
2682                }
2683            });
2684
2685            let offset = if direction == Direction::Prev {
2686                possibilities.max().unwrap_or(0)
2687            } else {
2688                possibilities.min().unwrap_or(map.buffer_snapshot.len())
2689            };
2690
2691            let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2692            if new_point == display_point {
2693                break;
2694            }
2695            display_point = new_point;
2696        }
2697        return display_point;
2698    };
2699
2700    for _ in 0..times {
2701        let point = map.display_point_to_point(display_point, Bias::Left);
2702        let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2703            return display_point;
2704        };
2705        let next_point = match (direction, is_start) {
2706            (Direction::Prev, true) => {
2707                let mut start = excerpt.start_anchor().to_display_point(&map);
2708                if start >= display_point && start.row() > DisplayRow(0) {
2709                    let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2710                        return display_point;
2711                    };
2712                    start = excerpt.start_anchor().to_display_point(&map);
2713                }
2714                start
2715            }
2716            (Direction::Prev, false) => {
2717                let mut start = excerpt.start_anchor().to_display_point(&map);
2718                if start.row() > DisplayRow(0) {
2719                    *start.row_mut() -= 1;
2720                }
2721                map.clip_point(start, Bias::Left)
2722            }
2723            (Direction::Next, true) => {
2724                let mut end = excerpt.end_anchor().to_display_point(&map);
2725                *end.row_mut() += 1;
2726                map.clip_point(end, Bias::Right)
2727            }
2728            (Direction::Next, false) => {
2729                let mut end = excerpt.end_anchor().to_display_point(&map);
2730                *end.column_mut() = 0;
2731                if end <= display_point {
2732                    *end.row_mut() += 1;
2733                    let point_end = map.display_point_to_point(end, Bias::Right);
2734                    let Some(excerpt) =
2735                        map.buffer_snapshot.excerpt_containing(point_end..point_end)
2736                    else {
2737                        return display_point;
2738                    };
2739                    end = excerpt.end_anchor().to_display_point(&map);
2740                    *end.column_mut() = 0;
2741                }
2742                end
2743            }
2744        };
2745        if next_point == display_point {
2746            break;
2747        }
2748        display_point = next_point;
2749    }
2750
2751    display_point
2752}
2753
2754#[cfg(test)]
2755mod test {
2756
2757    use crate::{
2758        state::Mode,
2759        test::{NeovimBackedTestContext, VimTestContext},
2760    };
2761    use editor::display_map::Inlay;
2762    use indoc::indoc;
2763
2764    #[gpui::test]
2765    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2766        let mut cx = NeovimBackedTestContext::new(cx).await;
2767
2768        let initial_state = indoc! {r"ˇabc
2769            def
2770
2771            paragraph
2772            the second
2773
2774
2775
2776            third and
2777            final"};
2778
2779        // goes down once
2780        cx.set_shared_state(initial_state).await;
2781        cx.simulate_shared_keystrokes("}").await;
2782        cx.shared_state().await.assert_eq(indoc! {r"abc
2783            def
2784            ˇ
2785            paragraph
2786            the second
2787
2788
2789
2790            third and
2791            final"});
2792
2793        // goes up once
2794        cx.simulate_shared_keystrokes("{").await;
2795        cx.shared_state().await.assert_eq(initial_state);
2796
2797        // goes down twice
2798        cx.simulate_shared_keystrokes("2 }").await;
2799        cx.shared_state().await.assert_eq(indoc! {r"abc
2800            def
2801
2802            paragraph
2803            the second
2804            ˇ
2805
2806
2807            third and
2808            final"});
2809
2810        // goes down over multiple blanks
2811        cx.simulate_shared_keystrokes("}").await;
2812        cx.shared_state().await.assert_eq(indoc! {r"abc
2813                def
2814
2815                paragraph
2816                the second
2817
2818
2819
2820                third and
2821                finaˇl"});
2822
2823        // goes up twice
2824        cx.simulate_shared_keystrokes("2 {").await;
2825        cx.shared_state().await.assert_eq(indoc! {r"abc
2826                def
2827                ˇ
2828                paragraph
2829                the second
2830
2831
2832
2833                third and
2834                final"});
2835    }
2836
2837    #[gpui::test]
2838    async fn test_matching(cx: &mut gpui::TestAppContext) {
2839        let mut cx = NeovimBackedTestContext::new(cx).await;
2840
2841        cx.set_shared_state(indoc! {r"func ˇ(a string) {
2842                do(something(with<Types>.and_arrays[0, 2]))
2843            }"})
2844            .await;
2845        cx.simulate_shared_keystrokes("%").await;
2846        cx.shared_state()
2847            .await
2848            .assert_eq(indoc! {r"func (a stringˇ) {
2849                do(something(with<Types>.and_arrays[0, 2]))
2850            }"});
2851
2852        // test it works on the last character of the line
2853        cx.set_shared_state(indoc! {r"func (a string) ˇ{
2854            do(something(with<Types>.and_arrays[0, 2]))
2855            }"})
2856            .await;
2857        cx.simulate_shared_keystrokes("%").await;
2858        cx.shared_state()
2859            .await
2860            .assert_eq(indoc! {r"func (a string) {
2861            do(something(with<Types>.and_arrays[0, 2]))
2862            ˇ}"});
2863
2864        // test it works on immediate nesting
2865        cx.set_shared_state("ˇ{()}").await;
2866        cx.simulate_shared_keystrokes("%").await;
2867        cx.shared_state().await.assert_eq("{()ˇ}");
2868        cx.simulate_shared_keystrokes("%").await;
2869        cx.shared_state().await.assert_eq("ˇ{()}");
2870
2871        // test it works on immediate nesting inside braces
2872        cx.set_shared_state("{\n    ˇ{()}\n}").await;
2873        cx.simulate_shared_keystrokes("%").await;
2874        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
2875
2876        // test it jumps to the next paren on a line
2877        cx.set_shared_state("func ˇboop() {\n}").await;
2878        cx.simulate_shared_keystrokes("%").await;
2879        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2880    }
2881
2882    #[gpui::test]
2883    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2884        let mut cx = NeovimBackedTestContext::new(cx).await;
2885
2886        // test it works with curly braces
2887        cx.set_shared_state(indoc! {r"func (a string) {
2888                do(something(with<Types>.anˇd_arrays[0, 2]))
2889            }"})
2890            .await;
2891        cx.simulate_shared_keystrokes("] }").await;
2892        cx.shared_state()
2893            .await
2894            .assert_eq(indoc! {r"func (a string) {
2895                do(something(with<Types>.and_arrays[0, 2]))
2896            ˇ}"});
2897
2898        // test it works with brackets
2899        cx.set_shared_state(indoc! {r"func (a string) {
2900                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2901            }"})
2902            .await;
2903        cx.simulate_shared_keystrokes("] )").await;
2904        cx.shared_state()
2905            .await
2906            .assert_eq(indoc! {r"func (a string) {
2907                do(something(with<Types>.and_arrays[0, 2])ˇ)
2908            }"});
2909
2910        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2911            .await;
2912        cx.simulate_shared_keystrokes("] )").await;
2913        cx.shared_state()
2914            .await
2915            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2916
2917        // test it works on immediate nesting
2918        cx.set_shared_state("{ˇ {}{}}").await;
2919        cx.simulate_shared_keystrokes("] }").await;
2920        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2921        cx.set_shared_state("(ˇ ()())").await;
2922        cx.simulate_shared_keystrokes("] )").await;
2923        cx.shared_state().await.assert_eq("( ()()ˇ)");
2924
2925        // test it works on immediate nesting inside braces
2926        cx.set_shared_state("{\n    ˇ {()}\n}").await;
2927        cx.simulate_shared_keystrokes("] }").await;
2928        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
2929        cx.set_shared_state("(\n    ˇ {()}\n)").await;
2930        cx.simulate_shared_keystrokes("] )").await;
2931        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
2932    }
2933
2934    #[gpui::test]
2935    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2936        let mut cx = NeovimBackedTestContext::new(cx).await;
2937
2938        // test it works with curly braces
2939        cx.set_shared_state(indoc! {r"func (a string) {
2940                do(something(with<Types>.anˇd_arrays[0, 2]))
2941            }"})
2942            .await;
2943        cx.simulate_shared_keystrokes("[ {").await;
2944        cx.shared_state()
2945            .await
2946            .assert_eq(indoc! {r"func (a string) ˇ{
2947                do(something(with<Types>.and_arrays[0, 2]))
2948            }"});
2949
2950        // test it works with brackets
2951        cx.set_shared_state(indoc! {r"func (a string) {
2952                do(somethiˇng(with<Types>.and_arrays[0, 2]))
2953            }"})
2954            .await;
2955        cx.simulate_shared_keystrokes("[ (").await;
2956        cx.shared_state()
2957            .await
2958            .assert_eq(indoc! {r"func (a string) {
2959                doˇ(something(with<Types>.and_arrays[0, 2]))
2960            }"});
2961
2962        // test it works on immediate nesting
2963        cx.set_shared_state("{{}{} ˇ }").await;
2964        cx.simulate_shared_keystrokes("[ {").await;
2965        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
2966        cx.set_shared_state("(()() ˇ )").await;
2967        cx.simulate_shared_keystrokes("[ (").await;
2968        cx.shared_state().await.assert_eq("ˇ(()()  )");
2969
2970        // test it works on immediate nesting inside braces
2971        cx.set_shared_state("{\n    {()} ˇ\n}").await;
2972        cx.simulate_shared_keystrokes("[ {").await;
2973        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
2974        cx.set_shared_state("(\n    {()} ˇ\n)").await;
2975        cx.simulate_shared_keystrokes("[ (").await;
2976        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
2977    }
2978
2979    #[gpui::test]
2980    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2981        let mut cx = NeovimBackedTestContext::new_html(cx).await;
2982
2983        cx.neovim.exec("set filetype=html").await;
2984
2985        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2986        cx.simulate_shared_keystrokes("%").await;
2987        cx.shared_state()
2988            .await
2989            .assert_eq(indoc! {r"<body><ˇ/body>"});
2990        cx.simulate_shared_keystrokes("%").await;
2991
2992        // test jumping backwards
2993        cx.shared_state()
2994            .await
2995            .assert_eq(indoc! {r"<ˇbody></body>"});
2996
2997        // test self-closing tags
2998        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2999        cx.simulate_shared_keystrokes("%").await;
3000        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3001
3002        // test tag with attributes
3003        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3004            </div>
3005            "})
3006            .await;
3007        cx.simulate_shared_keystrokes("%").await;
3008        cx.shared_state()
3009            .await
3010            .assert_eq(indoc! {r"<div class='test' id='main'>
3011            <ˇ/div>
3012            "});
3013
3014        // test multi-line self-closing tag
3015        cx.set_shared_state(indoc! {r#"<a>
3016            <br
3017                test = "test"
3018            /ˇ>
3019        </a>"#})
3020            .await;
3021        cx.simulate_shared_keystrokes("%").await;
3022        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3023            ˇ<br
3024                test = "test"
3025            />
3026        </a>"#});
3027    }
3028
3029    #[gpui::test]
3030    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3031        let mut cx = NeovimBackedTestContext::new(cx).await;
3032
3033        // f and F
3034        cx.set_shared_state("ˇone two three four").await;
3035        cx.simulate_shared_keystrokes("f o").await;
3036        cx.shared_state().await.assert_eq("one twˇo three four");
3037        cx.simulate_shared_keystrokes(",").await;
3038        cx.shared_state().await.assert_eq("ˇone two three four");
3039        cx.simulate_shared_keystrokes("2 ;").await;
3040        cx.shared_state().await.assert_eq("one two three fˇour");
3041        cx.simulate_shared_keystrokes("shift-f e").await;
3042        cx.shared_state().await.assert_eq("one two threˇe four");
3043        cx.simulate_shared_keystrokes("2 ;").await;
3044        cx.shared_state().await.assert_eq("onˇe two three four");
3045        cx.simulate_shared_keystrokes(",").await;
3046        cx.shared_state().await.assert_eq("one two thrˇee four");
3047
3048        // t and T
3049        cx.set_shared_state("ˇone two three four").await;
3050        cx.simulate_shared_keystrokes("t o").await;
3051        cx.shared_state().await.assert_eq("one tˇwo three four");
3052        cx.simulate_shared_keystrokes(",").await;
3053        cx.shared_state().await.assert_eq("oˇne two three four");
3054        cx.simulate_shared_keystrokes("2 ;").await;
3055        cx.shared_state().await.assert_eq("one two three ˇfour");
3056        cx.simulate_shared_keystrokes("shift-t e").await;
3057        cx.shared_state().await.assert_eq("one two threeˇ four");
3058        cx.simulate_shared_keystrokes("3 ;").await;
3059        cx.shared_state().await.assert_eq("oneˇ two three four");
3060        cx.simulate_shared_keystrokes(",").await;
3061        cx.shared_state().await.assert_eq("one two thˇree four");
3062    }
3063
3064    #[gpui::test]
3065    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3066        let mut cx = NeovimBackedTestContext::new(cx).await;
3067        let initial_state = indoc! {r"something(ˇfoo)"};
3068        cx.set_shared_state(initial_state).await;
3069        cx.simulate_shared_keystrokes("}").await;
3070        cx.shared_state().await.assert_eq("something(fooˇ)");
3071    }
3072
3073    #[gpui::test]
3074    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3075        let mut cx = NeovimBackedTestContext::new(cx).await;
3076        cx.set_shared_state("ˇone\n  two\nthree").await;
3077        cx.simulate_shared_keystrokes("enter").await;
3078        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3079    }
3080
3081    #[gpui::test]
3082    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3083        let mut cx = NeovimBackedTestContext::new(cx).await;
3084        cx.set_shared_state("ˇ one\n two \nthree").await;
3085        cx.simulate_shared_keystrokes("g _").await;
3086        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3087
3088        cx.set_shared_state("ˇ one \n two \nthree").await;
3089        cx.simulate_shared_keystrokes("g _").await;
3090        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3091        cx.simulate_shared_keystrokes("2 g _").await;
3092        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3093    }
3094
3095    #[gpui::test]
3096    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3097        let mut cx = NeovimBackedTestContext::new(cx).await;
3098        let initial_state = indoc! {r"abc
3099          def
3100          paragraph
3101          the second
3102          third ˇand
3103          final"};
3104
3105        cx.set_shared_state(initial_state).await;
3106        cx.simulate_shared_keystrokes("shift-h").await;
3107        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3108          def
3109          paragraph
3110          the second
3111          third and
3112          final"});
3113
3114        // clip point
3115        cx.set_shared_state(indoc! {r"
3116          1 2 3
3117          4 5 6
3118          7 8 ˇ9
3119          "})
3120            .await;
3121        cx.simulate_shared_keystrokes("shift-h").await;
3122        cx.shared_state().await.assert_eq(indoc! {"
3123          1 2 ˇ3
3124          4 5 6
3125          7 8 9
3126          "});
3127
3128        cx.set_shared_state(indoc! {r"
3129          1 2 3
3130          4 5 6
3131          ˇ7 8 9
3132          "})
3133            .await;
3134        cx.simulate_shared_keystrokes("shift-h").await;
3135        cx.shared_state().await.assert_eq(indoc! {"
3136          ˇ1 2 3
3137          4 5 6
3138          7 8 9
3139          "});
3140
3141        cx.set_shared_state(indoc! {r"
3142          1 2 3
3143          4 5 ˇ6
3144          7 8 9"})
3145            .await;
3146        cx.simulate_shared_keystrokes("9 shift-h").await;
3147        cx.shared_state().await.assert_eq(indoc! {"
3148          1 2 3
3149          4 5 6
3150          7 8 ˇ9"});
3151    }
3152
3153    #[gpui::test]
3154    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3155        let mut cx = NeovimBackedTestContext::new(cx).await;
3156        let initial_state = indoc! {r"abˇc
3157          def
3158          paragraph
3159          the second
3160          third and
3161          final"};
3162
3163        cx.set_shared_state(initial_state).await;
3164        cx.simulate_shared_keystrokes("shift-m").await;
3165        cx.shared_state().await.assert_eq(indoc! {r"abc
3166          def
3167          paˇragraph
3168          the second
3169          third and
3170          final"});
3171
3172        cx.set_shared_state(indoc! {r"
3173          1 2 3
3174          4 5 6
3175          7 8 ˇ9
3176          "})
3177            .await;
3178        cx.simulate_shared_keystrokes("shift-m").await;
3179        cx.shared_state().await.assert_eq(indoc! {"
3180          1 2 3
3181          4 5 ˇ6
3182          7 8 9
3183          "});
3184        cx.set_shared_state(indoc! {r"
3185          1 2 3
3186          4 5 6
3187          ˇ7 8 9
3188          "})
3189            .await;
3190        cx.simulate_shared_keystrokes("shift-m").await;
3191        cx.shared_state().await.assert_eq(indoc! {"
3192          1 2 3
3193          ˇ4 5 6
3194          7 8 9
3195          "});
3196        cx.set_shared_state(indoc! {r"
3197          ˇ1 2 3
3198          4 5 6
3199          7 8 9
3200          "})
3201            .await;
3202        cx.simulate_shared_keystrokes("shift-m").await;
3203        cx.shared_state().await.assert_eq(indoc! {"
3204          1 2 3
3205          ˇ4 5 6
3206          7 8 9
3207          "});
3208        cx.set_shared_state(indoc! {r"
3209          1 2 3
3210          ˇ4 5 6
3211          7 8 9
3212          "})
3213            .await;
3214        cx.simulate_shared_keystrokes("shift-m").await;
3215        cx.shared_state().await.assert_eq(indoc! {"
3216          1 2 3
3217          ˇ4 5 6
3218          7 8 9
3219          "});
3220        cx.set_shared_state(indoc! {r"
3221          1 2 3
3222          4 5 ˇ6
3223          7 8 9
3224          "})
3225            .await;
3226        cx.simulate_shared_keystrokes("shift-m").await;
3227        cx.shared_state().await.assert_eq(indoc! {"
3228          1 2 3
3229          4 5 ˇ6
3230          7 8 9
3231          "});
3232    }
3233
3234    #[gpui::test]
3235    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3236        let mut cx = NeovimBackedTestContext::new(cx).await;
3237        let initial_state = indoc! {r"abc
3238          deˇf
3239          paragraph
3240          the second
3241          third and
3242          final"};
3243
3244        cx.set_shared_state(initial_state).await;
3245        cx.simulate_shared_keystrokes("shift-l").await;
3246        cx.shared_state().await.assert_eq(indoc! {r"abc
3247          def
3248          paragraph
3249          the second
3250          third and
3251          fiˇnal"});
3252
3253        cx.set_shared_state(indoc! {r"
3254          1 2 3
3255          4 5 ˇ6
3256          7 8 9
3257          "})
3258            .await;
3259        cx.simulate_shared_keystrokes("shift-l").await;
3260        cx.shared_state().await.assert_eq(indoc! {"
3261          1 2 3
3262          4 5 6
3263          7 8 9
3264          ˇ"});
3265
3266        cx.set_shared_state(indoc! {r"
3267          1 2 3
3268          ˇ4 5 6
3269          7 8 9
3270          "})
3271            .await;
3272        cx.simulate_shared_keystrokes("shift-l").await;
3273        cx.shared_state().await.assert_eq(indoc! {"
3274          1 2 3
3275          4 5 6
3276          7 8 9
3277          ˇ"});
3278
3279        cx.set_shared_state(indoc! {r"
3280          1 2 ˇ3
3281          4 5 6
3282          7 8 9
3283          "})
3284            .await;
3285        cx.simulate_shared_keystrokes("shift-l").await;
3286        cx.shared_state().await.assert_eq(indoc! {"
3287          1 2 3
3288          4 5 6
3289          7 8 9
3290          ˇ"});
3291
3292        cx.set_shared_state(indoc! {r"
3293          ˇ1 2 3
3294          4 5 6
3295          7 8 9
3296          "})
3297            .await;
3298        cx.simulate_shared_keystrokes("shift-l").await;
3299        cx.shared_state().await.assert_eq(indoc! {"
3300          1 2 3
3301          4 5 6
3302          7 8 9
3303          ˇ"});
3304
3305        cx.set_shared_state(indoc! {r"
3306          1 2 3
3307          4 5 ˇ6
3308          7 8 9
3309          "})
3310            .await;
3311        cx.simulate_shared_keystrokes("9 shift-l").await;
3312        cx.shared_state().await.assert_eq(indoc! {"
3313          1 2 ˇ3
3314          4 5 6
3315          7 8 9
3316          "});
3317    }
3318
3319    #[gpui::test]
3320    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3321        let mut cx = NeovimBackedTestContext::new(cx).await;
3322        cx.set_shared_state(indoc! {r"
3323        456 5ˇ67 678
3324        "})
3325            .await;
3326        cx.simulate_shared_keystrokes("g e").await;
3327        cx.shared_state().await.assert_eq(indoc! {"
3328        45ˇ6 567 678
3329        "});
3330
3331        // Test times
3332        cx.set_shared_state(indoc! {r"
3333        123 234 345
3334        456 5ˇ67 678
3335        "})
3336            .await;
3337        cx.simulate_shared_keystrokes("4 g e").await;
3338        cx.shared_state().await.assert_eq(indoc! {"
3339        12ˇ3 234 345
3340        456 567 678
3341        "});
3342
3343        // With punctuation
3344        cx.set_shared_state(indoc! {r"
3345        123 234 345
3346        4;5.6 5ˇ67 678
3347        789 890 901
3348        "})
3349            .await;
3350        cx.simulate_shared_keystrokes("g e").await;
3351        cx.shared_state().await.assert_eq(indoc! {"
3352          123 234 345
3353          4;5.ˇ6 567 678
3354          789 890 901
3355        "});
3356
3357        // With punctuation and count
3358        cx.set_shared_state(indoc! {r"
3359        123 234 345
3360        4;5.6 5ˇ67 678
3361        789 890 901
3362        "})
3363            .await;
3364        cx.simulate_shared_keystrokes("5 g e").await;
3365        cx.shared_state().await.assert_eq(indoc! {"
3366          123 234 345
3367          ˇ4;5.6 567 678
3368          789 890 901
3369        "});
3370
3371        // newlines
3372        cx.set_shared_state(indoc! {r"
3373        123 234 345
3374
3375        78ˇ9 890 901
3376        "})
3377            .await;
3378        cx.simulate_shared_keystrokes("g e").await;
3379        cx.shared_state().await.assert_eq(indoc! {"
3380          123 234 345
3381          ˇ
3382          789 890 901
3383        "});
3384        cx.simulate_shared_keystrokes("g e").await;
3385        cx.shared_state().await.assert_eq(indoc! {"
3386          123 234 34ˇ5
3387
3388          789 890 901
3389        "});
3390
3391        // With punctuation
3392        cx.set_shared_state(indoc! {r"
3393        123 234 345
3394        4;5.ˇ6 567 678
3395        789 890 901
3396        "})
3397            .await;
3398        cx.simulate_shared_keystrokes("g shift-e").await;
3399        cx.shared_state().await.assert_eq(indoc! {"
3400          123 234 34ˇ5
3401          4;5.6 567 678
3402          789 890 901
3403        "});
3404    }
3405
3406    #[gpui::test]
3407    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3408        let mut cx = NeovimBackedTestContext::new(cx).await;
3409
3410        cx.set_shared_state(indoc! {"
3411            fn aˇ() {
3412              return
3413            }
3414        "})
3415            .await;
3416        cx.simulate_shared_keystrokes("v $ %").await;
3417        cx.shared_state().await.assert_eq(indoc! {"
3418            fn a«() {
3419              return
3420            }ˇ»
3421        "});
3422    }
3423
3424    #[gpui::test]
3425    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3426        let mut cx = VimTestContext::new(cx, true).await;
3427
3428        cx.set_state(
3429            indoc! {"
3430            struct Foo {
3431            ˇ
3432            }
3433        "},
3434            Mode::Normal,
3435        );
3436
3437        cx.update_editor(|editor, _window, cx| {
3438            let range = editor.selections.newest_anchor().range();
3439            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3440            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3441            editor.splice_inlays(vec![], vec![inlay], cx);
3442        });
3443
3444        cx.simulate_keystrokes("j");
3445        cx.assert_state(
3446            indoc! {"
3447            struct Foo {
3448
3449            ˇ}
3450        "},
3451            Mode::Normal,
3452        );
3453    }
3454}