motion.rs

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