object.rs

   1use std::ops::Range;
   2
   3use crate::{
   4    Vim,
   5    motion::{is_subword_end, is_subword_start, right},
   6    state::{Mode, Operator},
   7};
   8use editor::{
   9    Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, ToOffset,
  10    display_map::{DisplaySnapshot, ToDisplayPoint},
  11    movement::{self, FindRange},
  12};
  13use gpui::{Action, Window, actions};
  14use itertools::Itertools;
  15use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
  16use multi_buffer::MultiBufferRow;
  17use schemars::JsonSchema;
  18use serde::Deserialize;
  19use ui::Context;
  20
  21#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
  22#[serde(rename_all = "snake_case")]
  23pub enum Object {
  24    Word { ignore_punctuation: bool },
  25    Subword { ignore_punctuation: bool },
  26    Sentence,
  27    Paragraph,
  28    Quotes,
  29    BackQuotes,
  30    AnyQuotes,
  31    MiniQuotes,
  32    DoubleQuotes,
  33    VerticalBars,
  34    AnyBrackets,
  35    MiniBrackets,
  36    Parentheses,
  37    SquareBrackets,
  38    CurlyBrackets,
  39    AngleBrackets,
  40    Argument,
  41    IndentObj { include_below: bool },
  42    Tag,
  43    Method,
  44    Class,
  45    Comment,
  46    EntireFile,
  47}
  48
  49/// Selects a word text object.
  50#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  51#[action(namespace = vim)]
  52#[serde(deny_unknown_fields)]
  53struct Word {
  54    #[serde(default)]
  55    ignore_punctuation: bool,
  56}
  57
  58/// Selects a subword text object.
  59#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  60#[action(namespace = vim)]
  61#[serde(deny_unknown_fields)]
  62struct Subword {
  63    #[serde(default)]
  64    ignore_punctuation: bool,
  65}
  66/// Selects text at the same indentation level.
  67#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  68#[action(namespace = vim)]
  69#[serde(deny_unknown_fields)]
  70struct IndentObj {
  71    #[serde(default)]
  72    include_below: bool,
  73}
  74
  75#[derive(Debug, Clone)]
  76pub struct CandidateRange {
  77    pub start: DisplayPoint,
  78    pub end: DisplayPoint,
  79}
  80
  81#[derive(Debug, Clone)]
  82pub struct CandidateWithRanges {
  83    candidate: CandidateRange,
  84    open_range: Range<MultiBufferOffset>,
  85    close_range: Range<MultiBufferOffset>,
  86}
  87
  88/// Selects text at the same indentation level.
  89#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  90#[action(namespace = vim)]
  91#[serde(deny_unknown_fields)]
  92struct Parentheses {
  93    #[serde(default)]
  94    opening: bool,
  95}
  96
  97/// Selects text at the same indentation level.
  98#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
  99#[action(namespace = vim)]
 100#[serde(deny_unknown_fields)]
 101struct SquareBrackets {
 102    #[serde(default)]
 103    opening: bool,
 104}
 105
 106/// Selects text at the same indentation level.
 107#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 108#[action(namespace = vim)]
 109#[serde(deny_unknown_fields)]
 110struct AngleBrackets {
 111    #[serde(default)]
 112    opening: bool,
 113}
 114/// Selects text at the same indentation level.
 115#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 116#[action(namespace = vim)]
 117#[serde(deny_unknown_fields)]
 118struct CurlyBrackets {
 119    #[serde(default)]
 120    opening: bool,
 121}
 122
 123fn cover_or_next<I: Iterator<Item = (Range<MultiBufferOffset>, Range<MultiBufferOffset>)>>(
 124    candidates: Option<I>,
 125    caret: DisplayPoint,
 126    map: &DisplaySnapshot,
 127) -> Option<CandidateWithRanges> {
 128    let caret_offset = caret.to_offset(map, Bias::Left);
 129    let mut covering = vec![];
 130    let mut next_ones = vec![];
 131    let snapshot = map.buffer_snapshot();
 132
 133    if let Some(ranges) = candidates {
 134        for (open_range, close_range) in ranges {
 135            let start_off = open_range.start;
 136            let end_off = close_range.end;
 137            let candidate = CandidateWithRanges {
 138                candidate: CandidateRange {
 139                    start: start_off.to_display_point(map),
 140                    end: end_off.to_display_point(map),
 141                },
 142                open_range: open_range.clone(),
 143                close_range: close_range.clone(),
 144            };
 145
 146            if open_range
 147                .start
 148                .to_offset(snapshot)
 149                .to_display_point(map)
 150                .row()
 151                == caret_offset.to_display_point(map).row()
 152            {
 153                if start_off <= caret_offset && caret_offset < end_off {
 154                    covering.push(candidate);
 155                } else if start_off >= caret_offset {
 156                    next_ones.push(candidate);
 157                }
 158            }
 159        }
 160    }
 161
 162    // 1) covering -> smallest width
 163    if !covering.is_empty() {
 164        return covering.into_iter().min_by_key(|r| {
 165            r.candidate.end.to_offset(map, Bias::Right)
 166                - r.candidate.start.to_offset(map, Bias::Left)
 167        });
 168    }
 169
 170    // 2) next -> closest by start
 171    if !next_ones.is_empty() {
 172        return next_ones.into_iter().min_by_key(|r| {
 173            let start = r.candidate.start.to_offset(map, Bias::Left);
 174            (start.0 as isize - caret_offset.0 as isize).abs()
 175        });
 176    }
 177
 178    None
 179}
 180
 181type DelimiterPredicate = dyn Fn(&BufferSnapshot, usize, usize) -> bool;
 182
 183struct DelimiterRange {
 184    open: Range<MultiBufferOffset>,
 185    close: Range<MultiBufferOffset>,
 186}
 187
 188impl DelimiterRange {
 189    fn to_display_range(&self, map: &DisplaySnapshot, around: bool) -> Range<DisplayPoint> {
 190        if around {
 191            self.open.start.to_display_point(map)..self.close.end.to_display_point(map)
 192        } else {
 193            self.open.end.to_display_point(map)..self.close.start.to_display_point(map)
 194        }
 195    }
 196}
 197
 198fn find_mini_delimiters(
 199    map: &DisplaySnapshot,
 200    display_point: DisplayPoint,
 201    around: bool,
 202    is_valid_delimiter: &DelimiterPredicate,
 203) -> Option<Range<DisplayPoint>> {
 204    let point = map.clip_at_line_end(display_point).to_point(map);
 205    let offset = point.to_offset(&map.buffer_snapshot());
 206
 207    let line_range = get_line_range(map, point);
 208    let visible_line_range = get_visible_line_range(&line_range);
 209
 210    let snapshot = &map.buffer_snapshot();
 211    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 212    let buffer = excerpt.buffer();
 213    let buffer_offset = excerpt.map_offset_to_buffer(offset);
 214
 215    let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 216        is_valid_delimiter(buffer, open.start, close.start)
 217    };
 218
 219    // Try to find delimiters in visible range first
 220    let ranges = map
 221        .buffer_snapshot()
 222        .bracket_ranges(visible_line_range)
 223        .map(|ranges| {
 224            ranges.filter_map(|(open, close)| {
 225                // Convert the ranges from multibuffer space to buffer space as
 226                // that is what `is_valid_delimiter` expects, otherwise it might
 227                // panic as the values might be out of bounds.
 228                let buffer_open = excerpt.map_range_to_buffer(open.clone());
 229                let buffer_close = excerpt.map_range_to_buffer(close.clone());
 230
 231                if is_valid_delimiter(buffer, buffer_open.start.0, buffer_close.start.0) {
 232                    Some((open, close))
 233                } else {
 234                    None
 235                }
 236            })
 237        });
 238
 239    if let Some(candidate) = cover_or_next(ranges, display_point, map) {
 240        return Some(
 241            DelimiterRange {
 242                open: candidate.open_range,
 243                close: candidate.close_range,
 244            }
 245            .to_display_range(map, around),
 246        );
 247    }
 248
 249    // Fall back to innermost enclosing brackets
 250    let (open_bracket, close_bracket) = buffer
 251        .innermost_enclosing_bracket_ranges(buffer_offset..buffer_offset, Some(&bracket_filter))?;
 252
 253    Some(
 254        DelimiterRange {
 255            open: excerpt.map_range_from_buffer(
 256                BufferOffset(open_bracket.start)..BufferOffset(open_bracket.end),
 257            ),
 258            close: excerpt.map_range_from_buffer(
 259                BufferOffset(close_bracket.start)..BufferOffset(close_bracket.end),
 260            ),
 261        }
 262        .to_display_range(map, around),
 263    )
 264}
 265
 266fn get_line_range(map: &DisplaySnapshot, point: Point) -> Range<Point> {
 267    let (start, mut end) = (
 268        map.prev_line_boundary(point).0,
 269        map.next_line_boundary(point).0,
 270    );
 271
 272    if end == point {
 273        end = map.max_point().to_point(map);
 274    }
 275
 276    start..end
 277}
 278
 279fn get_visible_line_range(line_range: &Range<Point>) -> Range<Point> {
 280    let end_column = line_range.end.column.saturating_sub(1);
 281    line_range.start..Point::new(line_range.end.row, end_column)
 282}
 283
 284fn is_quote_delimiter(buffer: &BufferSnapshot, _start: usize, end: usize) -> bool {
 285    matches!(buffer.chars_at(end).next(), Some('\'' | '"' | '`'))
 286}
 287
 288fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> bool {
 289    matches!(
 290        buffer.chars_at(start).next(),
 291        Some('(' | '[' | '{' | '<' | '|')
 292    )
 293}
 294
 295fn find_mini_quotes(
 296    map: &DisplaySnapshot,
 297    display_point: DisplayPoint,
 298    around: bool,
 299) -> Option<Range<DisplayPoint>> {
 300    find_mini_delimiters(map, display_point, around, &is_quote_delimiter)
 301}
 302
 303fn find_mini_brackets(
 304    map: &DisplaySnapshot,
 305    display_point: DisplayPoint,
 306    around: bool,
 307) -> Option<Range<DisplayPoint>> {
 308    find_mini_delimiters(map, display_point, around, &is_bracket_delimiter)
 309}
 310
 311actions!(
 312    vim,
 313    [
 314        /// Selects a sentence text object.
 315        Sentence,
 316        /// Selects a paragraph text object.
 317        Paragraph,
 318        /// Selects text within single quotes.
 319        Quotes,
 320        /// Selects text within backticks.
 321        BackQuotes,
 322        /// Selects text within the nearest quotes (single or double).
 323        MiniQuotes,
 324        /// Selects text within any type of quotes.
 325        AnyQuotes,
 326        /// Selects text within double quotes.
 327        DoubleQuotes,
 328        /// Selects text within vertical bars (pipes).
 329        VerticalBars,
 330        /// Selects text within the nearest brackets.
 331        MiniBrackets,
 332        /// Selects text within any type of brackets.
 333        AnyBrackets,
 334        /// Selects a function argument.
 335        Argument,
 336        /// Selects an HTML/XML tag.
 337        Tag,
 338        /// Selects a method or function.
 339        Method,
 340        /// Selects a class definition.
 341        Class,
 342        /// Selects a comment block.
 343        Comment,
 344        /// Selects the entire file.
 345        EntireFile
 346    ]
 347);
 348
 349pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 350    Vim::action(
 351        editor,
 352        cx,
 353        |vim, &Word { ignore_punctuation }: &Word, window, cx| {
 354            vim.object(Object::Word { ignore_punctuation }, window, cx)
 355        },
 356    );
 357    Vim::action(
 358        editor,
 359        cx,
 360        |vim, &Subword { ignore_punctuation }: &Subword, window, cx| {
 361            vim.object(Object::Subword { ignore_punctuation }, window, cx)
 362        },
 363    );
 364    Vim::action(editor, cx, |vim, _: &Tag, window, cx| {
 365        vim.object(Object::Tag, window, cx)
 366    });
 367    Vim::action(editor, cx, |vim, _: &Sentence, window, cx| {
 368        vim.object(Object::Sentence, window, cx)
 369    });
 370    Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| {
 371        vim.object(Object::Paragraph, window, cx)
 372    });
 373    Vim::action(editor, cx, |vim, _: &Quotes, window, cx| {
 374        vim.object(Object::Quotes, window, cx)
 375    });
 376    Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
 377        vim.object(Object::BackQuotes, window, cx)
 378    });
 379    Vim::action(editor, cx, |vim, _: &MiniQuotes, window, cx| {
 380        vim.object(Object::MiniQuotes, window, cx)
 381    });
 382    Vim::action(editor, cx, |vim, _: &MiniBrackets, window, cx| {
 383        vim.object(Object::MiniBrackets, window, cx)
 384    });
 385    Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
 386        vim.object(Object::AnyQuotes, window, cx)
 387    });
 388    Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| {
 389        vim.object(Object::AnyBrackets, window, cx)
 390    });
 391    Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
 392        vim.object(Object::BackQuotes, window, cx)
 393    });
 394    Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
 395        vim.object(Object::DoubleQuotes, window, cx)
 396    });
 397    Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| {
 398        vim.object_impl(Object::Parentheses, action.opening, window, cx)
 399    });
 400    Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| {
 401        vim.object_impl(Object::SquareBrackets, action.opening, window, cx)
 402    });
 403    Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| {
 404        vim.object_impl(Object::CurlyBrackets, action.opening, window, cx)
 405    });
 406    Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| {
 407        vim.object_impl(Object::AngleBrackets, action.opening, window, cx)
 408    });
 409    Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
 410        vim.object(Object::VerticalBars, window, cx)
 411    });
 412    Vim::action(editor, cx, |vim, _: &Argument, window, cx| {
 413        vim.object(Object::Argument, window, cx)
 414    });
 415    Vim::action(editor, cx, |vim, _: &Method, window, cx| {
 416        vim.object(Object::Method, window, cx)
 417    });
 418    Vim::action(editor, cx, |vim, _: &Class, window, cx| {
 419        vim.object(Object::Class, window, cx)
 420    });
 421    Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| {
 422        vim.object(Object::EntireFile, window, cx)
 423    });
 424    Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
 425        if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
 426            vim.push_operator(Operator::Object { around: true }, window, cx);
 427        }
 428        vim.object(Object::Comment, window, cx)
 429    });
 430    Vim::action(
 431        editor,
 432        cx,
 433        |vim, &IndentObj { include_below }: &IndentObj, window, cx| {
 434            vim.object(Object::IndentObj { include_below }, window, cx)
 435        },
 436    );
 437}
 438
 439impl Vim {
 440    fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
 441        self.object_impl(object, false, window, cx);
 442    }
 443
 444    fn object_impl(
 445        &mut self,
 446        object: Object,
 447        opening: bool,
 448        window: &mut Window,
 449        cx: &mut Context<Self>,
 450    ) {
 451        let count = Self::take_count(cx);
 452
 453        match self.mode {
 454            Mode::Normal | Mode::HelixNormal => {
 455                self.normal_object(object, count, opening, window, cx)
 456            }
 457            Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => {
 458                self.visual_object(object, count, window, cx)
 459            }
 460            Mode::Insert | Mode::Replace => {
 461                // Shouldn't execute a text object in insert mode. Ignoring
 462            }
 463        }
 464    }
 465}
 466
 467impl Object {
 468    pub fn is_multiline(self) -> bool {
 469        match self {
 470            Object::Word { .. }
 471            | Object::Subword { .. }
 472            | Object::Quotes
 473            | Object::BackQuotes
 474            | Object::AnyQuotes
 475            | Object::MiniQuotes
 476            | Object::VerticalBars
 477            | Object::DoubleQuotes => false,
 478            Object::Sentence
 479            | Object::Paragraph
 480            | Object::AnyBrackets
 481            | Object::MiniBrackets
 482            | Object::Parentheses
 483            | Object::Tag
 484            | Object::AngleBrackets
 485            | Object::CurlyBrackets
 486            | Object::SquareBrackets
 487            | Object::Argument
 488            | Object::Method
 489            | Object::Class
 490            | Object::EntireFile
 491            | Object::Comment
 492            | Object::IndentObj { .. } => true,
 493        }
 494    }
 495
 496    pub fn always_expands_both_ways(self) -> bool {
 497        match self {
 498            Object::Word { .. }
 499            | Object::Subword { .. }
 500            | Object::Sentence
 501            | Object::Paragraph
 502            | Object::Argument
 503            | Object::IndentObj { .. } => false,
 504            Object::Quotes
 505            | Object::BackQuotes
 506            | Object::AnyQuotes
 507            | Object::MiniQuotes
 508            | Object::DoubleQuotes
 509            | Object::VerticalBars
 510            | Object::AnyBrackets
 511            | Object::MiniBrackets
 512            | Object::Parentheses
 513            | Object::SquareBrackets
 514            | Object::Tag
 515            | Object::Method
 516            | Object::Class
 517            | Object::Comment
 518            | Object::EntireFile
 519            | Object::CurlyBrackets
 520            | Object::AngleBrackets => true,
 521        }
 522    }
 523
 524    pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
 525        match self {
 526            Object::Word { .. }
 527            | Object::Subword { .. }
 528            | Object::Sentence
 529            | Object::Quotes
 530            | Object::AnyQuotes
 531            | Object::MiniQuotes
 532            | Object::BackQuotes
 533            | Object::DoubleQuotes => {
 534                if current_mode == Mode::VisualBlock {
 535                    Mode::VisualBlock
 536                } else {
 537                    Mode::Visual
 538                }
 539            }
 540            Object::Parentheses
 541            | Object::AnyBrackets
 542            | Object::MiniBrackets
 543            | Object::SquareBrackets
 544            | Object::CurlyBrackets
 545            | Object::AngleBrackets
 546            | Object::VerticalBars
 547            | Object::Tag
 548            | Object::Comment
 549            | Object::Argument
 550            | Object::IndentObj { .. } => Mode::Visual,
 551            Object::Method | Object::Class => {
 552                if around {
 553                    Mode::VisualLine
 554                } else {
 555                    Mode::Visual
 556                }
 557            }
 558            Object::Paragraph | Object::EntireFile => Mode::VisualLine,
 559        }
 560    }
 561
 562    pub fn range(
 563        self,
 564        map: &DisplaySnapshot,
 565        selection: Selection<DisplayPoint>,
 566        around: bool,
 567        times: Option<usize>,
 568    ) -> Option<Range<DisplayPoint>> {
 569        let relative_to = selection.head();
 570        match self {
 571            Object::Word { ignore_punctuation } => {
 572                let count = times.unwrap_or(1);
 573                if around {
 574                    around_word(map, relative_to, ignore_punctuation, count)
 575                } else {
 576                    in_word(map, relative_to, ignore_punctuation, count).map(|range| {
 577                        // For iw with count > 1, vim includes trailing whitespace
 578                        if count > 1 {
 579                            let spans_multiple_lines = range.start.row() != range.end.row();
 580                            expand_to_include_whitespace(map, range, !spans_multiple_lines)
 581                        } else {
 582                            range
 583                        }
 584                    })
 585                }
 586            }
 587            Object::Subword { ignore_punctuation } => {
 588                if around {
 589                    around_subword(map, relative_to, ignore_punctuation)
 590                } else {
 591                    in_subword(map, relative_to, ignore_punctuation)
 592                }
 593            }
 594            Object::Sentence => sentence(map, relative_to, around),
 595            //change others later
 596            Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
 597            Object::Quotes => {
 598                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 599            }
 600            Object::BackQuotes => {
 601                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 602            }
 603            Object::AnyQuotes => {
 604                let quote_types = ['\'', '"', '`'];
 605                let cursor_offset = relative_to.to_offset(map, Bias::Left);
 606
 607                // Find innermost range directly without collecting all ranges
 608                let mut innermost = None;
 609                let mut min_size = usize::MAX;
 610
 611                // First pass: find innermost enclosing range
 612                for quote in quote_types {
 613                    if let Some(range) = surrounding_markers(
 614                        map,
 615                        relative_to,
 616                        around,
 617                        self.is_multiline(),
 618                        quote,
 619                        quote,
 620                    ) {
 621                        let start_offset = range.start.to_offset(map, Bias::Left);
 622                        let end_offset = range.end.to_offset(map, Bias::Right);
 623
 624                        if cursor_offset >= start_offset && cursor_offset <= end_offset {
 625                            let size = end_offset - start_offset;
 626                            if size < min_size {
 627                                min_size = size;
 628                                innermost = Some(range);
 629                            }
 630                        }
 631                    }
 632                }
 633
 634                if let Some(range) = innermost {
 635                    return Some(range);
 636                }
 637
 638                // Fallback: find nearest pair if not inside any quotes
 639                quote_types
 640                    .iter()
 641                    .flat_map(|&quote| {
 642                        surrounding_markers(
 643                            map,
 644                            relative_to,
 645                            around,
 646                            self.is_multiline(),
 647                            quote,
 648                            quote,
 649                        )
 650                    })
 651                    .min_by_key(|range| {
 652                        let start_offset = range.start.to_offset(map, Bias::Left);
 653                        let end_offset = range.end.to_offset(map, Bias::Right);
 654                        if cursor_offset < start_offset {
 655                            (start_offset - cursor_offset) as isize
 656                        } else if cursor_offset > end_offset {
 657                            (cursor_offset - end_offset) as isize
 658                        } else {
 659                            0
 660                        }
 661                    })
 662            }
 663            Object::MiniQuotes => find_mini_quotes(map, relative_to, around),
 664            Object::DoubleQuotes => {
 665                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 666            }
 667            Object::VerticalBars => {
 668                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 669            }
 670            Object::Parentheses => {
 671                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 672            }
 673            Object::Tag => {
 674                let head = selection.head();
 675                let range = selection.range();
 676                surrounding_html_tag(map, head, range, around)
 677            }
 678            Object::AnyBrackets => {
 679                let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
 680                let cursor_offset = relative_to.to_offset(map, Bias::Left);
 681
 682                // Find innermost enclosing bracket range
 683                let mut innermost = None;
 684                let mut min_size = usize::MAX;
 685
 686                for &(open, close) in bracket_pairs.iter() {
 687                    if let Some(range) = surrounding_markers(
 688                        map,
 689                        relative_to,
 690                        around,
 691                        self.is_multiline(),
 692                        open,
 693                        close,
 694                    ) {
 695                        let start_offset = range.start.to_offset(map, Bias::Left);
 696                        let end_offset = range.end.to_offset(map, Bias::Right);
 697
 698                        if cursor_offset >= start_offset && cursor_offset <= end_offset {
 699                            let size = end_offset - start_offset;
 700                            if size < min_size {
 701                                min_size = size;
 702                                innermost = Some(range);
 703                            }
 704                        }
 705                    }
 706                }
 707
 708                if let Some(range) = innermost {
 709                    return Some(range);
 710                }
 711
 712                // Fallback: find nearest bracket pair if not inside any
 713                bracket_pairs
 714                    .iter()
 715                    .flat_map(|&(open, close)| {
 716                        surrounding_markers(
 717                            map,
 718                            relative_to,
 719                            around,
 720                            self.is_multiline(),
 721                            open,
 722                            close,
 723                        )
 724                    })
 725                    .min_by_key(|range| {
 726                        let start_offset = range.start.to_offset(map, Bias::Left);
 727                        let end_offset = range.end.to_offset(map, Bias::Right);
 728                        if cursor_offset < start_offset {
 729                            (start_offset - cursor_offset) as isize
 730                        } else if cursor_offset > end_offset {
 731                            (cursor_offset - end_offset) as isize
 732                        } else {
 733                            0
 734                        }
 735                    })
 736            }
 737            Object::MiniBrackets => find_mini_brackets(map, relative_to, around),
 738            Object::SquareBrackets => {
 739                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 740            }
 741            Object::CurlyBrackets => {
 742                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 743            }
 744            Object::AngleBrackets => {
 745                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 746            }
 747            Object::Method => text_object(
 748                map,
 749                relative_to,
 750                if around {
 751                    TextObject::AroundFunction
 752                } else {
 753                    TextObject::InsideFunction
 754                },
 755            ),
 756            Object::Comment => text_object(
 757                map,
 758                relative_to,
 759                if around {
 760                    TextObject::AroundComment
 761                } else {
 762                    TextObject::InsideComment
 763                },
 764            ),
 765            Object::Class => text_object(
 766                map,
 767                relative_to,
 768                if around {
 769                    TextObject::AroundClass
 770                } else {
 771                    TextObject::InsideClass
 772                },
 773            ),
 774            Object::Argument => argument(map, relative_to, around),
 775            Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
 776            Object::EntireFile => entire_file(map),
 777        }
 778    }
 779
 780    pub fn expand_selection(
 781        self,
 782        map: &DisplaySnapshot,
 783        selection: &mut Selection<DisplayPoint>,
 784        around: bool,
 785        times: Option<usize>,
 786    ) -> bool {
 787        if let Some(range) = self.range(map, selection.clone(), around, times) {
 788            selection.start = range.start;
 789            selection.end = range.end;
 790            true
 791        } else {
 792            false
 793        }
 794    }
 795}
 796
 797/// Returns a range that surrounds the word `relative_to` is in.
 798///
 799/// If `relative_to` is at the start of a word, return the word.
 800/// If `relative_to` is between words, return the space between.
 801/// If `times` > 1, extend to include additional words.
 802fn in_word(
 803    map: &DisplaySnapshot,
 804    relative_to: DisplayPoint,
 805    ignore_punctuation: bool,
 806    times: usize,
 807) -> Option<Range<DisplayPoint>> {
 808    // Use motion::right so that we consider the character under the cursor when looking for the start
 809    let classifier = map
 810        .buffer_snapshot()
 811        .char_classifier_at(relative_to.to_point(map))
 812        .ignore_punctuation(ignore_punctuation);
 813    let start = movement::find_preceding_boundary_display_point(
 814        map,
 815        right(map, relative_to, 1),
 816        movement::FindRange::SingleLine,
 817        |left, right| classifier.kind(left) != classifier.kind(right),
 818    );
 819
 820    let mut end =
 821        movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 822            classifier.kind(left) != classifier.kind(right)
 823        });
 824
 825    let is_boundary = |left: char, right: char| classifier.kind(left) != classifier.kind(right);
 826
 827    for _ in 1..times {
 828        let kind_at_end = map
 829            .buffer_chars_at(end.to_offset(map, Bias::Right))
 830            .next()
 831            .map(|(c, _)| classifier.kind(c));
 832
 833        // Skip whitespace but not punctuation (punctuation is its own word unit).
 834        let next_end = if kind_at_end == Some(CharKind::Whitespace) {
 835            let after_whitespace =
 836                movement::find_boundary(map, end, FindRange::MultiLine, is_boundary);
 837            movement::find_boundary(map, after_whitespace, FindRange::MultiLine, is_boundary)
 838        } else {
 839            movement::find_boundary(map, end, FindRange::MultiLine, is_boundary)
 840        };
 841        if next_end == end {
 842            break;
 843        }
 844        end = next_end;
 845    }
 846
 847    Some(start..end)
 848}
 849
 850fn in_subword(
 851    map: &DisplaySnapshot,
 852    relative_to: DisplayPoint,
 853    ignore_punctuation: bool,
 854) -> Option<Range<DisplayPoint>> {
 855    let offset = relative_to.to_offset(map, Bias::Left);
 856    // Use motion::right so that we consider the character under the cursor when looking for the start
 857    let classifier = map
 858        .buffer_snapshot()
 859        .char_classifier_at(relative_to.to_point(map))
 860        .ignore_punctuation(ignore_punctuation);
 861    let in_subword = map
 862        .buffer_chars_at(offset)
 863        .next()
 864        .map(|(c, _)| {
 865            let is_separator = "._-".contains(c);
 866            !classifier.is_whitespace(c) && !is_separator
 867        })
 868        .unwrap_or(false);
 869
 870    let start = if in_subword {
 871        movement::find_preceding_boundary_display_point(
 872            map,
 873            right(map, relative_to, 1),
 874            movement::FindRange::SingleLine,
 875            |left, right| {
 876                let is_word_start = classifier.kind(left) != classifier.kind(right);
 877                is_word_start || is_subword_start(left, right, "._-")
 878            },
 879        )
 880    } else {
 881        movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 882            let is_word_start = classifier.kind(left) != classifier.kind(right);
 883            is_word_start || is_subword_start(left, right, "._-")
 884        })
 885    };
 886
 887    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 888        let is_word_end = classifier.kind(left) != classifier.kind(right);
 889        is_word_end || is_subword_end(left, right, "._-")
 890    });
 891
 892    Some(start..end)
 893}
 894
 895pub fn surrounding_html_tag(
 896    map: &DisplaySnapshot,
 897    head: DisplayPoint,
 898    range: Range<DisplayPoint>,
 899    around: bool,
 900) -> Option<Range<DisplayPoint>> {
 901    fn read_tag(chars: impl Iterator<Item = char>) -> String {
 902        chars
 903            .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
 904            .collect()
 905    }
 906    fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 907        if Some('<') != chars.next() {
 908            return None;
 909        }
 910        Some(read_tag(chars))
 911    }
 912    fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 913        if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
 914            return None;
 915        }
 916        Some(read_tag(chars))
 917    }
 918
 919    let snapshot = &map.buffer_snapshot();
 920    let offset = head.to_offset(map, Bias::Left);
 921    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 922    let buffer = excerpt.buffer();
 923    let offset = excerpt.map_offset_to_buffer(offset);
 924
 925    // Find the most closest to current offset
 926    let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
 927    let mut last_child_node = cursor.node();
 928    while cursor.goto_first_child_for_byte(offset.0).is_some() {
 929        last_child_node = cursor.node();
 930    }
 931
 932    let mut last_child_node = Some(last_child_node);
 933    while let Some(cur_node) = last_child_node {
 934        if cur_node.child_count() >= 2 {
 935            let first_child = cur_node.child(0);
 936            let last_child = cur_node.child(cur_node.child_count() as u32 - 1);
 937            if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
 938                let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
 939                let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
 940                // It needs to be handled differently according to the selection length
 941                let is_valid = if range.end.to_offset(map, Bias::Left)
 942                    - range.start.to_offset(map, Bias::Left)
 943                    <= 1
 944                {
 945                    offset.0 <= last_child.end_byte()
 946                } else {
 947                    excerpt
 948                        .map_offset_to_buffer(range.start.to_offset(map, Bias::Left))
 949                        .0
 950                        >= first_child.start_byte()
 951                        && excerpt
 952                            .map_offset_to_buffer(range.end.to_offset(map, Bias::Left))
 953                            .0
 954                            <= last_child.start_byte() + 1
 955                };
 956                if open_tag.is_some() && open_tag == close_tag && is_valid {
 957                    let range = if around {
 958                        first_child.byte_range().start..last_child.byte_range().end
 959                    } else {
 960                        first_child.byte_range().end..last_child.byte_range().start
 961                    };
 962                    let range = BufferOffset(range.start)..BufferOffset(range.end);
 963                    if excerpt.contains_buffer_range(range.clone()) {
 964                        let result = excerpt.map_range_from_buffer(range);
 965                        return Some(
 966                            result.start.to_display_point(map)..result.end.to_display_point(map),
 967                        );
 968                    }
 969                }
 970            }
 971        }
 972        last_child_node = cur_node.parent();
 973    }
 974    None
 975}
 976
 977/// Returns a range that surrounds the word and following whitespace
 978/// relative_to is in.
 979///
 980/// If `relative_to` is at the start of a word, return the word and following whitespace.
 981/// If `relative_to` is between words, return the whitespace back and the following word.
 982///
 983/// if in word
 984///   delete that word
 985///   if there is whitespace following the word, delete that as well
 986///   otherwise, delete any preceding whitespace
 987/// otherwise
 988///   delete whitespace around cursor
 989///   delete word following the cursor
 990/// If `times` > 1, extend to include additional words.
 991fn around_word(
 992    map: &DisplaySnapshot,
 993    relative_to: DisplayPoint,
 994    ignore_punctuation: bool,
 995    times: usize,
 996) -> Option<Range<DisplayPoint>> {
 997    let offset = relative_to.to_offset(map, Bias::Left);
 998    let classifier = map
 999        .buffer_snapshot()
1000        .char_classifier_at(offset)
1001        .ignore_punctuation(ignore_punctuation);
1002    let in_word = map
1003        .buffer_chars_at(offset)
1004        .next()
1005        .map(|(c, _)| !classifier.is_whitespace(c))
1006        .unwrap_or(false);
1007
1008    if in_word {
1009        around_containing_word(map, relative_to, ignore_punctuation, times)
1010    } else {
1011        around_next_word(map, relative_to, ignore_punctuation, times)
1012    }
1013}
1014
1015fn around_subword(
1016    map: &DisplaySnapshot,
1017    relative_to: DisplayPoint,
1018    ignore_punctuation: bool,
1019) -> Option<Range<DisplayPoint>> {
1020    // Use motion::right so that we consider the character under the cursor when looking for the start
1021    let classifier = map
1022        .buffer_snapshot()
1023        .char_classifier_at(relative_to.to_point(map))
1024        .ignore_punctuation(ignore_punctuation);
1025    let start = movement::find_preceding_boundary_display_point(
1026        map,
1027        right(map, relative_to, 1),
1028        movement::FindRange::SingleLine,
1029        |left, right| {
1030            let is_separator = |c: char| "._-".contains(c);
1031            let is_word_start =
1032                classifier.kind(left) != classifier.kind(right) && !is_separator(left);
1033            is_word_start || is_subword_start(left, right, "._-")
1034        },
1035    );
1036
1037    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
1038        let is_separator = |c: char| "._-".contains(c);
1039        let is_word_end = classifier.kind(left) != classifier.kind(right) && !is_separator(right);
1040        is_word_end || is_subword_end(left, right, "._-")
1041    });
1042
1043    Some(start..end).map(|range| expand_to_include_whitespace(map, range, true))
1044}
1045
1046fn around_containing_word(
1047    map: &DisplaySnapshot,
1048    relative_to: DisplayPoint,
1049    ignore_punctuation: bool,
1050    times: usize,
1051) -> Option<Range<DisplayPoint>> {
1052    in_word(map, relative_to, ignore_punctuation, times).map(|range| {
1053        let spans_multiple_lines = range.start.row() != range.end.row();
1054        let stop_at_newline = !spans_multiple_lines;
1055
1056        let line_start = DisplayPoint::new(range.start.row(), 0);
1057        let is_first_word = map
1058            .buffer_chars_at(line_start.to_offset(map, Bias::Left))
1059            .take_while(|(ch, offset)| {
1060                offset < &range.start.to_offset(map, Bias::Left) && ch.is_whitespace()
1061            })
1062            .count()
1063            > 0;
1064
1065        if is_first_word {
1066            // For first word on line, trim indentation
1067            let mut expanded = expand_to_include_whitespace(map, range.clone(), stop_at_newline);
1068            expanded.start = range.start;
1069            expanded
1070        } else {
1071            expand_to_include_whitespace(map, range, stop_at_newline)
1072        }
1073    })
1074}
1075
1076fn around_next_word(
1077    map: &DisplaySnapshot,
1078    relative_to: DisplayPoint,
1079    ignore_punctuation: bool,
1080    times: usize,
1081) -> Option<Range<DisplayPoint>> {
1082    let classifier = map
1083        .buffer_snapshot()
1084        .char_classifier_at(relative_to.to_point(map))
1085        .ignore_punctuation(ignore_punctuation);
1086    let start = movement::find_preceding_boundary_display_point(
1087        map,
1088        right(map, relative_to, 1),
1089        FindRange::SingleLine,
1090        |left, right| classifier.kind(left) != classifier.kind(right),
1091    );
1092
1093    let mut word_found = false;
1094    let mut end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
1095        let left_kind = classifier.kind(left);
1096        let right_kind = classifier.kind(right);
1097
1098        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
1099
1100        if right_kind != CharKind::Whitespace {
1101            word_found = true;
1102        }
1103
1104        found
1105    });
1106
1107    for _ in 1..times {
1108        let next_end = movement::find_boundary(map, end, FindRange::MultiLine, |left, right| {
1109            let left_kind = classifier.kind(left);
1110            let right_kind = classifier.kind(right);
1111
1112            let in_word_unit = left_kind != CharKind::Whitespace;
1113            (in_word_unit && left_kind != right_kind) || right == '\n' && left == '\n'
1114        });
1115        if next_end == end {
1116            break;
1117        }
1118        end = next_end;
1119    }
1120
1121    Some(start..end)
1122}
1123
1124fn entire_file(map: &DisplaySnapshot) -> Option<Range<DisplayPoint>> {
1125    Some(DisplayPoint::zero()..map.max_point())
1126}
1127
1128fn text_object(
1129    map: &DisplaySnapshot,
1130    relative_to: DisplayPoint,
1131    target: TextObject,
1132) -> Option<Range<DisplayPoint>> {
1133    let snapshot = &map.buffer_snapshot();
1134    let offset = relative_to.to_offset(map, Bias::Left);
1135
1136    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
1137    let buffer = excerpt.buffer();
1138    let offset = excerpt.map_offset_to_buffer(offset);
1139
1140    let mut matches: Vec<Range<usize>> = buffer
1141        .text_object_ranges(offset..offset, TreeSitterOptions::default())
1142        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1143        .collect();
1144    matches.sort_by_key(|r| r.end - r.start);
1145    if let Some(buffer_range) = matches.first() {
1146        let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end);
1147        let range = excerpt.map_range_from_buffer(buffer_range);
1148        return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
1149    }
1150
1151    let around = target.around()?;
1152    let mut matches: Vec<Range<usize>> = buffer
1153        .text_object_ranges(offset..offset, TreeSitterOptions::default())
1154        .filter_map(|(r, m)| if m == around { Some(r) } else { None })
1155        .collect();
1156    matches.sort_by_key(|r| r.end - r.start);
1157    let around_range = matches.first()?;
1158
1159    let mut matches: Vec<Range<usize>> = buffer
1160        .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
1161        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
1162        .collect();
1163    matches.sort_by_key(|r| r.start);
1164    if let Some(buffer_range) = matches.first()
1165        && !buffer_range.is_empty()
1166    {
1167        let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end);
1168        let range = excerpt.map_range_from_buffer(buffer_range);
1169        return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
1170    }
1171    let around_range = BufferOffset(around_range.start)..BufferOffset(around_range.end);
1172    let buffer_range = excerpt.map_range_from_buffer(around_range);
1173    return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
1174}
1175
1176fn argument(
1177    map: &DisplaySnapshot,
1178    relative_to: DisplayPoint,
1179    around: bool,
1180) -> Option<Range<DisplayPoint>> {
1181    let snapshot = &map.buffer_snapshot();
1182    let offset = relative_to.to_offset(map, Bias::Left);
1183
1184    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
1185    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
1186    let buffer = excerpt.buffer();
1187
1188    fn comma_delimited_range_at(
1189        buffer: &BufferSnapshot,
1190        mut offset: BufferOffset,
1191        include_comma: bool,
1192    ) -> Option<Range<BufferOffset>> {
1193        // Seek to the first non-whitespace character
1194        offset += buffer
1195            .chars_at(offset)
1196            .take_while(|c| c.is_whitespace())
1197            .map(char::len_utf8)
1198            .sum::<usize>();
1199
1200        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
1201            // Filter out empty ranges
1202            if open.end == close.start {
1203                return false;
1204            }
1205
1206            // If the cursor is outside the brackets, ignore them
1207            if open.start == offset.0 || close.end == offset.0 {
1208                return false;
1209            }
1210
1211            // TODO: Is there any better way to filter out string brackets?
1212            // Used to filter out string brackets
1213            matches!(
1214                buffer.chars_at(open.start).next(),
1215                Some('(' | '[' | '{' | '<' | '|')
1216            )
1217        };
1218
1219        // Find the brackets containing the cursor
1220        let (open_bracket, close_bracket) =
1221            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
1222
1223        let inner_bracket_range = BufferOffset(open_bracket.end)..BufferOffset(close_bracket.start);
1224
1225        let layer = buffer.syntax_layer_at(offset)?;
1226        let node = layer.node();
1227        let mut cursor = node.walk();
1228
1229        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
1230        let mut parent_covers_bracket_range = false;
1231        loop {
1232            let node = cursor.node();
1233            let range = node.byte_range();
1234            let covers_bracket_range =
1235                range.start == open_bracket.start && range.end == close_bracket.end;
1236            if parent_covers_bracket_range && !covers_bracket_range {
1237                break;
1238            }
1239            parent_covers_bracket_range = covers_bracket_range;
1240
1241            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
1242            cursor.goto_first_child_for_byte(offset.0)?;
1243        }
1244
1245        let mut argument_node = cursor.node();
1246
1247        // If the child node is the open bracket, move to the next sibling.
1248        if argument_node.byte_range() == open_bracket {
1249            if !cursor.goto_next_sibling() {
1250                return Some(inner_bracket_range);
1251            }
1252            argument_node = cursor.node();
1253        }
1254        // While the child node is the close bracket or a comma, move to the previous sibling
1255        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
1256            if !cursor.goto_previous_sibling() {
1257                return Some(inner_bracket_range);
1258            }
1259            argument_node = cursor.node();
1260            if argument_node.byte_range() == open_bracket {
1261                return Some(inner_bracket_range);
1262            }
1263        }
1264
1265        // The start and end of the argument range, defaulting to the start and end of the argument node
1266        let mut start = argument_node.start_byte();
1267        let mut end = argument_node.end_byte();
1268
1269        let mut needs_surrounding_comma = include_comma;
1270
1271        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
1272        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
1273        while cursor.goto_previous_sibling() {
1274            let prev = cursor.node();
1275
1276            if prev.start_byte() < open_bracket.end {
1277                start = open_bracket.end;
1278                break;
1279            } else if prev.kind() == "," {
1280                if needs_surrounding_comma {
1281                    start = prev.start_byte();
1282                    needs_surrounding_comma = false;
1283                }
1284                break;
1285            } else if prev.start_byte() < start {
1286                start = prev.start_byte();
1287            }
1288        }
1289
1290        // Do the same for the end of the argument, extending to next comma or the end of the argument list
1291        while cursor.goto_next_sibling() {
1292            let next = cursor.node();
1293
1294            if next.end_byte() > close_bracket.start {
1295                end = close_bracket.start;
1296                break;
1297            } else if next.kind() == "," {
1298                if needs_surrounding_comma {
1299                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
1300                    if let Some(next_arg) = next.next_sibling() {
1301                        end = next_arg.start_byte();
1302                    } else {
1303                        end = next.end_byte();
1304                    }
1305                }
1306                break;
1307            } else if next.end_byte() > end {
1308                end = next.end_byte();
1309            }
1310        }
1311
1312        Some(BufferOffset(start)..BufferOffset(end))
1313    }
1314
1315    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
1316
1317    if excerpt.contains_buffer_range(result.clone()) {
1318        let result = excerpt.map_range_from_buffer(result);
1319        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
1320    } else {
1321        None
1322    }
1323}
1324
1325fn indent(
1326    map: &DisplaySnapshot,
1327    relative_to: DisplayPoint,
1328    around: bool,
1329    include_below: bool,
1330) -> Option<Range<DisplayPoint>> {
1331    let point = relative_to.to_point(map);
1332    let row = point.row;
1333
1334    let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
1335
1336    // Loop backwards until we find a non-blank line with less indent
1337    let mut start_row = row;
1338    for prev_row in (0..row).rev() {
1339        let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
1340        if indent.is_line_empty() {
1341            continue;
1342        }
1343        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1344            if around {
1345                // When around is true, include the first line with less indent
1346                start_row = prev_row;
1347            }
1348            break;
1349        }
1350        start_row = prev_row;
1351    }
1352
1353    // Loop forwards until we find a non-blank line with less indent
1354    let mut end_row = row;
1355    let max_rows = map.buffer_snapshot().max_row().0;
1356    for next_row in (row + 1)..=max_rows {
1357        let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
1358        if indent.is_line_empty() {
1359            continue;
1360        }
1361        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
1362            if around && include_below {
1363                // When around is true and including below, include this line
1364                end_row = next_row;
1365            }
1366            break;
1367        }
1368        end_row = next_row;
1369    }
1370
1371    let end_len = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
1372    let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
1373    let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
1374    Some(start..end)
1375}
1376
1377fn sentence(
1378    map: &DisplaySnapshot,
1379    relative_to: DisplayPoint,
1380    around: bool,
1381) -> Option<Range<DisplayPoint>> {
1382    let mut start = None;
1383    let relative_offset = relative_to.to_offset(map, Bias::Left);
1384    let mut previous_end = relative_offset;
1385
1386    let mut chars = map.buffer_chars_at(previous_end).peekable();
1387
1388    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
1389    for (char, offset) in chars
1390        .peek()
1391        .cloned()
1392        .into_iter()
1393        .chain(map.reverse_buffer_chars_at(previous_end))
1394    {
1395        if is_sentence_end(map, offset) {
1396            break;
1397        }
1398
1399        if is_possible_sentence_start(char) {
1400            start = Some(offset);
1401        }
1402
1403        previous_end = offset;
1404    }
1405
1406    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
1407    let mut end = relative_offset;
1408    for (char, offset) in chars {
1409        if start.is_none() && is_possible_sentence_start(char) {
1410            if around {
1411                start = Some(offset);
1412                continue;
1413            } else {
1414                end = offset;
1415                break;
1416            }
1417        }
1418
1419        if char != '\n' {
1420            end = offset + char.len_utf8();
1421        }
1422
1423        if is_sentence_end(map, end) {
1424            break;
1425        }
1426    }
1427
1428    let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
1429    if around {
1430        range = expand_to_include_whitespace(map, range, false);
1431    }
1432
1433    Some(range)
1434}
1435
1436fn is_possible_sentence_start(character: char) -> bool {
1437    !character.is_whitespace() && character != '.'
1438}
1439
1440const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
1441const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
1442const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
1443fn is_sentence_end(map: &DisplaySnapshot, offset: MultiBufferOffset) -> bool {
1444    let mut next_chars = map.buffer_chars_at(offset).peekable();
1445    if let Some((char, _)) = next_chars.next() {
1446        // We are at a double newline. This position is a sentence end.
1447        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
1448            return true;
1449        }
1450
1451        // The next text is not a valid whitespace. This is not a sentence end
1452        if !SENTENCE_END_WHITESPACE.contains(&char) {
1453            return false;
1454        }
1455    }
1456
1457    for (char, _) in map.reverse_buffer_chars_at(offset) {
1458        if SENTENCE_END_PUNCTUATION.contains(&char) {
1459            return true;
1460        }
1461
1462        if !SENTENCE_END_FILLERS.contains(&char) {
1463            return false;
1464        }
1465    }
1466
1467    false
1468}
1469
1470/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
1471/// whitespace to the end first and falls back to the start if there was none.
1472pub fn expand_to_include_whitespace(
1473    map: &DisplaySnapshot,
1474    range: Range<DisplayPoint>,
1475    stop_at_newline: bool,
1476) -> Range<DisplayPoint> {
1477    let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
1478    let mut whitespace_included = false;
1479
1480    let chars = map.buffer_chars_at(range.end).peekable();
1481    for (char, offset) in chars {
1482        if char == '\n' && stop_at_newline {
1483            break;
1484        }
1485
1486        if char.is_whitespace() {
1487            if char != '\n' || !stop_at_newline {
1488                range.end = offset + char.len_utf8();
1489                whitespace_included = true;
1490            }
1491        } else {
1492            // Found non whitespace. Quit out.
1493            break;
1494        }
1495    }
1496
1497    if !whitespace_included {
1498        for (char, point) in map.reverse_buffer_chars_at(range.start) {
1499            if char == '\n' && stop_at_newline {
1500                break;
1501            }
1502
1503            if !char.is_whitespace() {
1504                break;
1505            }
1506
1507            range.start = point;
1508        }
1509    }
1510
1511    range.start.to_display_point(map)..range.end.to_display_point(map)
1512}
1513
1514/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
1515/// where `relative_to` is in. If `around`, principally returns the range ending
1516/// at the end of the next paragraph.
1517///
1518/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
1519/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
1520/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
1521/// the trailing newline is not subject to subsequent operations).
1522///
1523/// Edge cases:
1524/// - If `around` and if the current paragraph is the last paragraph of the
1525///   file and is blank, then the selection results in an error.
1526/// - If `around` and if the current paragraph is the last paragraph of the
1527///   file and is not blank, then the returned range starts at the start of the
1528///   previous paragraph, if it exists.
1529fn paragraph(
1530    map: &DisplaySnapshot,
1531    relative_to: DisplayPoint,
1532    around: bool,
1533    times: usize,
1534) -> Option<Range<DisplayPoint>> {
1535    let mut paragraph_start = start_of_paragraph(map, relative_to);
1536    let mut paragraph_end = end_of_paragraph(map, relative_to);
1537
1538    for i in 0..times {
1539        let paragraph_end_row = paragraph_end.row();
1540        let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
1541        let point = relative_to.to_point(map);
1542        let current_line_is_empty = map
1543            .buffer_snapshot()
1544            .is_line_blank(MultiBufferRow(point.row));
1545
1546        if around {
1547            if paragraph_ends_with_eof {
1548                if current_line_is_empty {
1549                    return None;
1550                }
1551
1552                let paragraph_start_buffer_point = paragraph_start.to_point(map);
1553                if paragraph_start_buffer_point.row != 0 {
1554                    let previous_paragraph_last_line_start =
1555                        Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map);
1556                    paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1557                }
1558            } else {
1559                let paragraph_end_buffer_point = paragraph_end.to_point(map);
1560                let mut start_row = paragraph_end_buffer_point.row + 1;
1561                if i > 0 {
1562                    start_row += 1;
1563                }
1564                let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
1565                paragraph_end = end_of_paragraph(map, next_paragraph_start);
1566            }
1567        }
1568    }
1569
1570    let range = paragraph_start..paragraph_end;
1571    Some(range)
1572}
1573
1574/// Returns a position of the start of the current paragraph, where a paragraph
1575/// is defined as a run of non-blank lines or a run of blank lines.
1576pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1577    let point = display_point.to_point(map);
1578    if point.row == 0 {
1579        return DisplayPoint::zero();
1580    }
1581
1582    let is_current_line_blank = map
1583        .buffer_snapshot()
1584        .is_line_blank(MultiBufferRow(point.row));
1585
1586    for row in (0..point.row).rev() {
1587        let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1588        if blank != is_current_line_blank {
1589            return Point::new(row + 1, 0).to_display_point(map);
1590        }
1591    }
1592
1593    DisplayPoint::zero()
1594}
1595
1596/// Returns a position of the end of the current paragraph, where a paragraph
1597/// is defined as a run of non-blank lines or a run of blank lines.
1598/// The trailing newline is excluded from the paragraph.
1599pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1600    let point = display_point.to_point(map);
1601    if point.row == map.buffer_snapshot().max_row().0 {
1602        return map.max_point();
1603    }
1604
1605    let is_current_line_blank = map
1606        .buffer_snapshot()
1607        .is_line_blank(MultiBufferRow(point.row));
1608
1609    for row in point.row + 1..map.buffer_snapshot().max_row().0 + 1 {
1610        let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
1611        if blank != is_current_line_blank {
1612            let previous_row = row - 1;
1613            return Point::new(
1614                previous_row,
1615                map.buffer_snapshot().line_len(MultiBufferRow(previous_row)),
1616            )
1617            .to_display_point(map);
1618        }
1619    }
1620
1621    map.max_point()
1622}
1623
1624pub fn surrounding_markers(
1625    map: &DisplaySnapshot,
1626    relative_to: DisplayPoint,
1627    around: bool,
1628    search_across_lines: bool,
1629    open_marker: char,
1630    close_marker: char,
1631) -> Option<Range<DisplayPoint>> {
1632    let point = relative_to.to_offset(map, Bias::Left);
1633
1634    let mut matched_closes = 0;
1635    let mut opening = None;
1636
1637    let mut before_ch = match movement::chars_before(map, point).next() {
1638        Some((ch, _)) => ch,
1639        _ => '\0',
1640    };
1641    if let Some((ch, range)) = movement::chars_after(map, point).next()
1642        && ch == open_marker
1643        && before_ch != '\\'
1644    {
1645        if open_marker == close_marker {
1646            let mut total = 0;
1647            for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() {
1648                if ch == '\n' {
1649                    break;
1650                }
1651                if ch == open_marker && before_ch != '\\' {
1652                    total += 1;
1653                }
1654            }
1655            if total % 2 == 0 {
1656                opening = Some(range)
1657            }
1658        } else {
1659            opening = Some(range)
1660        }
1661    }
1662
1663    if opening.is_none() {
1664        let mut chars_before = movement::chars_before(map, point).peekable();
1665        while let Some((ch, range)) = chars_before.next() {
1666            if ch == '\n' && !search_across_lines {
1667                break;
1668            }
1669
1670            if let Some((before_ch, _)) = chars_before.peek()
1671                && *before_ch == '\\'
1672            {
1673                continue;
1674            }
1675
1676            if ch == open_marker {
1677                if matched_closes == 0 {
1678                    opening = Some(range);
1679                    break;
1680                }
1681                matched_closes -= 1;
1682            } else if ch == close_marker {
1683                matched_closes += 1
1684            }
1685        }
1686    }
1687    if opening.is_none() {
1688        for (ch, range) in movement::chars_after(map, point) {
1689            if before_ch != '\\' {
1690                if ch == open_marker {
1691                    opening = Some(range);
1692                    break;
1693                } else if ch == close_marker {
1694                    break;
1695                }
1696            }
1697
1698            before_ch = ch;
1699        }
1700    }
1701
1702    let mut opening = opening?;
1703
1704    let mut matched_opens = 0;
1705    let mut closing = None;
1706    before_ch = match movement::chars_before(map, opening.end).next() {
1707        Some((ch, _)) => ch,
1708        _ => '\0',
1709    };
1710    for (ch, range) in movement::chars_after(map, opening.end) {
1711        if ch == '\n' && !search_across_lines {
1712            break;
1713        }
1714
1715        if before_ch != '\\' {
1716            if ch == close_marker {
1717                if matched_opens == 0 {
1718                    closing = Some(range);
1719                    break;
1720                }
1721                matched_opens -= 1;
1722            } else if ch == open_marker {
1723                matched_opens += 1;
1724            }
1725        }
1726
1727        before_ch = ch;
1728    }
1729
1730    let mut closing = closing?;
1731
1732    if around && !search_across_lines {
1733        let mut found = false;
1734
1735        for (ch, range) in movement::chars_after(map, closing.end) {
1736            if ch.is_whitespace() && ch != '\n' {
1737                found = true;
1738                closing.end = range.end;
1739            } else {
1740                break;
1741            }
1742        }
1743
1744        if !found {
1745            for (ch, range) in movement::chars_before(map, opening.start) {
1746                if ch.is_whitespace() && ch != '\n' {
1747                    opening.start = range.start
1748                } else {
1749                    break;
1750                }
1751            }
1752        }
1753    }
1754
1755    // Adjust selection to remove leading and trailing whitespace for multiline inner brackets
1756    if !around && open_marker != close_marker {
1757        let start_point = opening.end.to_display_point(map);
1758        let end_point = closing.start.to_display_point(map);
1759        let start_offset = start_point.to_offset(map, Bias::Left);
1760        let end_offset = end_point.to_offset(map, Bias::Left);
1761
1762        if start_point.row() != end_point.row()
1763            && map
1764                .buffer_chars_at(start_offset)
1765                .take_while(|(_, offset)| offset < &end_offset)
1766                .any(|(ch, _)| !ch.is_whitespace())
1767        {
1768            let mut first_non_ws = None;
1769            let mut last_non_ws = None;
1770            for (ch, offset) in map.buffer_chars_at(start_offset) {
1771                if !ch.is_whitespace() {
1772                    first_non_ws = Some(offset);
1773                    break;
1774                }
1775            }
1776            for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
1777                if !ch.is_whitespace() {
1778                    last_non_ws = Some(offset + ch.len_utf8());
1779                    break;
1780                }
1781            }
1782            if let Some(start) = first_non_ws {
1783                opening.end = start;
1784            }
1785            if let Some(end) = last_non_ws {
1786                closing.start = end;
1787            }
1788        }
1789    }
1790
1791    let result = if around {
1792        opening.start..closing.end
1793    } else {
1794        opening.end..closing.start
1795    };
1796
1797    Some(
1798        map.clip_point(result.start.to_display_point(map), Bias::Left)
1799            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1800    )
1801}
1802
1803#[cfg(test)]
1804mod test {
1805    use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext};
1806    use gpui::KeyBinding;
1807    use indoc::indoc;
1808    use text::Point;
1809
1810    use crate::{
1811        object::{AnyBrackets, AnyQuotes, MiniBrackets},
1812        state::Mode,
1813        test::{NeovimBackedTestContext, VimTestContext},
1814    };
1815
1816    const WORD_LOCATIONS: &str = indoc! {"
1817        The quick ˇbrowˇnˇ•••
1818        fox ˇjuˇmpsˇ over
1819        the lazy dogˇ••
1820        ˇ
1821        ˇ
1822        ˇ
1823        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1824        ˇ••
1825        ˇ••
1826        ˇ  fox-jumpˇs over
1827        the lazy dogˇ•
1828        ˇ
1829        "
1830    };
1831
1832    #[gpui::test]
1833    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1834        let mut cx = NeovimBackedTestContext::new(cx).await;
1835
1836        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1837            .await
1838            .assert_matches();
1839        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1840            .await
1841            .assert_matches();
1842        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1843            .await
1844            .assert_matches();
1845        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1846            .await
1847            .assert_matches();
1848    }
1849
1850    #[gpui::test]
1851    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1852        let mut cx = NeovimBackedTestContext::new(cx).await;
1853
1854        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1855            .await
1856            .assert_matches();
1857        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1858            .await
1859            .assert_matches();
1860        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1861            .await
1862            .assert_matches();
1863        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1864            .await
1865            .assert_matches();
1866    }
1867
1868    #[gpui::test]
1869    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1870        let mut cx = NeovimBackedTestContext::new(cx).await;
1871
1872        /*
1873                cx.set_shared_state("The quick ˇbrown\nfox").await;
1874                cx.simulate_shared_keystrokes(["v"]).await;
1875                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1876                cx.simulate_shared_keystrokes(["i", "w"]).await;
1877                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1878        */
1879        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1880        cx.simulate_shared_keystrokes("v").await;
1881        cx.shared_state()
1882            .await
1883            .assert_eq("The quick brown\n«\nˇ»fox");
1884        cx.simulate_shared_keystrokes("i w").await;
1885        cx.shared_state()
1886            .await
1887            .assert_eq("The quick brown\n«\nˇ»fox");
1888
1889        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1890            .await
1891            .assert_matches();
1892        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1893            .await
1894            .assert_matches();
1895    }
1896
1897    #[gpui::test]
1898    async fn test_word_object_with_count(cx: &mut gpui::TestAppContext) {
1899        let mut cx = NeovimBackedTestContext::new(cx).await;
1900
1901        cx.set_shared_state("ˇone two three four").await;
1902        cx.simulate_shared_keystrokes("2 d a w").await;
1903        cx.shared_state().await.assert_matches();
1904
1905        cx.set_shared_state("ˇone two three four").await;
1906        cx.simulate_shared_keystrokes("d 2 a w").await;
1907        cx.shared_state().await.assert_matches();
1908
1909        // WORD (shift-w) ignores punctuation
1910        cx.set_shared_state("ˇone-two three-four five").await;
1911        cx.simulate_shared_keystrokes("2 d a shift-w").await;
1912        cx.shared_state().await.assert_matches();
1913
1914        cx.set_shared_state("ˇone two three four five").await;
1915        cx.simulate_shared_keystrokes("3 d a w").await;
1916        cx.shared_state().await.assert_matches();
1917
1918        // Multiplied counts: 2d2aw deletes 4 words (2*2)
1919        cx.set_shared_state("ˇone two three four five six").await;
1920        cx.simulate_shared_keystrokes("2 d 2 a w").await;
1921        cx.shared_state().await.assert_matches();
1922
1923        cx.set_shared_state("ˇone two three four").await;
1924        cx.simulate_shared_keystrokes("2 c a w").await;
1925        cx.shared_state().await.assert_matches();
1926
1927        cx.set_shared_state("ˇone two three four").await;
1928        cx.simulate_shared_keystrokes("2 y a w p").await;
1929        cx.shared_state().await.assert_matches();
1930
1931        // Punctuation: foo-bar is 3 word units (foo, -, bar), so 2aw selects "foo-"
1932        cx.set_shared_state("  ˇfoo-bar baz").await;
1933        cx.simulate_shared_keystrokes("2 d a w").await;
1934        cx.shared_state().await.assert_matches();
1935
1936        // Trailing whitespace counts as a word unit for iw
1937        cx.set_shared_state("ˇfoo   ").await;
1938        cx.simulate_shared_keystrokes("2 d i w").await;
1939        cx.shared_state().await.assert_matches();
1940
1941        // Multi-line: count > 1 crosses line boundaries
1942        cx.set_shared_state("ˇone\ntwo\nthree").await;
1943        cx.simulate_shared_keystrokes("2 d a w").await;
1944        cx.shared_state().await.assert_matches();
1945
1946        cx.set_shared_state("ˇone\ntwo\nthree\nfour").await;
1947        cx.simulate_shared_keystrokes("3 d a w").await;
1948        cx.shared_state().await.assert_matches();
1949
1950        cx.set_shared_state("ˇone\ntwo\nthree").await;
1951        cx.simulate_shared_keystrokes("2 d i w").await;
1952        cx.shared_state().await.assert_matches();
1953
1954        cx.set_shared_state("one ˇtwo\nthree four").await;
1955        cx.simulate_shared_keystrokes("2 d a w").await;
1956        cx.shared_state().await.assert_matches();
1957    }
1958
1959    const PARAGRAPH_EXAMPLES: &[&str] = &[
1960        // Single line
1961        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1962        // Multiple lines without empty lines
1963        indoc! {"
1964            ˇThe quick brownˇ
1965            ˇfox jumps overˇ
1966            the lazy dog.ˇ
1967        "},
1968        // Heading blank paragraph and trailing normal paragraph
1969        indoc! {"
1970            ˇ
1971            ˇ
1972            ˇThe quick brown fox jumps
1973            ˇover the lazy dog.
1974            ˇ
1975            ˇ
1976            ˇThe quick brown fox jumpsˇ
1977            ˇover the lazy dog.ˇ
1978        "},
1979        // Inserted blank paragraph and trailing blank paragraph
1980        indoc! {"
1981            ˇThe quick brown fox jumps
1982            ˇover the lazy dog.
1983            ˇ
1984            ˇ
1985            ˇ
1986            ˇThe quick brown fox jumpsˇ
1987            ˇover the lazy dog.ˇ
1988            ˇ
1989            ˇ
1990            ˇ
1991        "},
1992        // "Blank" paragraph with whitespace characters
1993        indoc! {"
1994            ˇThe quick brown fox jumps
1995            over the lazy dog.
1996
1997            ˇ \t
1998
1999            ˇThe quick brown fox jumps
2000            over the lazy dog.ˇ
2001            ˇ
2002            ˇ \t
2003            \t \t
2004        "},
2005        // Single line "paragraphs", where selection size might be zero.
2006        indoc! {"
2007            ˇThe quick brown fox jumps over the lazy dog.
2008            ˇ
2009            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
2010            ˇ
2011        "},
2012    ];
2013
2014    #[gpui::test]
2015    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
2016        let mut cx = NeovimBackedTestContext::new(cx).await;
2017
2018        for paragraph_example in PARAGRAPH_EXAMPLES {
2019            cx.simulate_at_each_offset("c i p", paragraph_example)
2020                .await
2021                .assert_matches();
2022            cx.simulate_at_each_offset("c a p", paragraph_example)
2023                .await
2024                .assert_matches();
2025        }
2026    }
2027
2028    #[gpui::test]
2029    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
2030        let mut cx = NeovimBackedTestContext::new(cx).await;
2031
2032        for paragraph_example in PARAGRAPH_EXAMPLES {
2033            cx.simulate_at_each_offset("d i p", paragraph_example)
2034                .await
2035                .assert_matches();
2036            cx.simulate_at_each_offset("d a p", paragraph_example)
2037                .await
2038                .assert_matches();
2039        }
2040    }
2041
2042    #[gpui::test]
2043    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
2044        let mut cx = NeovimBackedTestContext::new(cx).await;
2045
2046        const EXAMPLES: &[&str] = &[
2047            indoc! {"
2048                ˇThe quick brown
2049                fox jumps over
2050                the lazy dog.
2051            "},
2052            indoc! {"
2053                ˇ
2054
2055                ˇThe quick brown fox jumps
2056                over the lazy dog.
2057                ˇ
2058
2059                ˇThe quick brown fox jumps
2060                over the lazy dog.
2061            "},
2062            indoc! {"
2063                ˇThe quick brown fox jumps over the lazy dog.
2064                ˇ
2065                ˇThe quick brown fox jumps over the lazy dog.
2066
2067            "},
2068        ];
2069
2070        for paragraph_example in EXAMPLES {
2071            cx.simulate_at_each_offset("v i p", paragraph_example)
2072                .await
2073                .assert_matches();
2074            cx.simulate_at_each_offset("v a p", paragraph_example)
2075                .await
2076                .assert_matches();
2077        }
2078    }
2079
2080    #[gpui::test]
2081    async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2082        let mut cx = NeovimBackedTestContext::new(cx).await;
2083
2084        const WRAPPING_EXAMPLE: &str = indoc! {"
2085            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2086
2087            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2088
2089            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2090        "};
2091
2092        cx.set_shared_wrap(20).await;
2093
2094        cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE)
2095            .await
2096            .assert_matches();
2097        cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE)
2098            .await
2099            .assert_matches();
2100    }
2101
2102    #[gpui::test]
2103    async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2104        let mut cx = NeovimBackedTestContext::new(cx).await;
2105
2106        const WRAPPING_EXAMPLE: &str = indoc! {"
2107            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2108
2109            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2110
2111            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2112        "};
2113
2114        cx.set_shared_wrap(20).await;
2115
2116        cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE)
2117            .await
2118            .assert_matches();
2119        cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE)
2120            .await
2121            .assert_matches();
2122    }
2123
2124    #[gpui::test]
2125    async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) {
2126        let mut cx = NeovimBackedTestContext::new(cx).await;
2127
2128        cx.set_shared_state(indoc! {"
2129            a
2130                   ˇ•
2131            aaaaaaaaaaaaa
2132        "})
2133            .await;
2134
2135        cx.simulate_shared_keystrokes("d i p").await;
2136        cx.shared_state().await.assert_eq(indoc! {"
2137            a
2138            aaaaaaaˇaaaaaa
2139        "});
2140    }
2141
2142    #[gpui::test]
2143    async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
2144        let mut cx = NeovimBackedTestContext::new(cx).await;
2145
2146        const WRAPPING_EXAMPLE: &str = indoc! {"
2147            ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
2148
2149            ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
2150
2151            ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
2152        "};
2153
2154        cx.set_shared_wrap(20).await;
2155
2156        cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE)
2157            .await
2158            .assert_matches();
2159        cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE)
2160            .await
2161            .assert_matches();
2162    }
2163
2164    // Test string with "`" for opening surrounders and "'" for closing surrounders
2165    const SURROUNDING_MARKER_STRING: &str = indoc! {"
2166        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
2167        'ˇfox juˇmps ov`ˇer
2168        the ˇlazy d'o`ˇg"};
2169
2170    const SURROUNDING_OBJECTS: &[(char, char)] = &[
2171        ('"', '"'), // Double Quote
2172        ('(', ')'), // Parentheses
2173    ];
2174
2175    #[gpui::test]
2176    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2177        let mut cx = NeovimBackedTestContext::new(cx).await;
2178
2179        for (start, end) in SURROUNDING_OBJECTS {
2180            let marked_string = SURROUNDING_MARKER_STRING
2181                .replace('`', &start.to_string())
2182                .replace('\'', &end.to_string());
2183
2184            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
2185                .await
2186                .assert_matches();
2187            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
2188                .await
2189                .assert_matches();
2190            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
2191                .await
2192                .assert_matches();
2193            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
2194                .await
2195                .assert_matches();
2196        }
2197    }
2198    #[gpui::test]
2199    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2200        let mut cx = NeovimBackedTestContext::new(cx).await;
2201        cx.set_shared_wrap(12).await;
2202
2203        cx.set_shared_state(indoc! {
2204            "\"ˇhello world\"!"
2205        })
2206        .await;
2207        cx.simulate_shared_keystrokes("v i \"").await;
2208        cx.shared_state().await.assert_eq(indoc! {
2209            "\"«hello worldˇ»\"!"
2210        });
2211
2212        cx.set_shared_state(indoc! {
2213            "\"hˇello world\"!"
2214        })
2215        .await;
2216        cx.simulate_shared_keystrokes("v i \"").await;
2217        cx.shared_state().await.assert_eq(indoc! {
2218            "\"«hello worldˇ»\"!"
2219        });
2220
2221        cx.set_shared_state(indoc! {
2222            "helˇlo \"world\"!"
2223        })
2224        .await;
2225        cx.simulate_shared_keystrokes("v i \"").await;
2226        cx.shared_state().await.assert_eq(indoc! {
2227            "hello \"«worldˇ»\"!"
2228        });
2229
2230        cx.set_shared_state(indoc! {
2231            "hello \"wˇorld\"!"
2232        })
2233        .await;
2234        cx.simulate_shared_keystrokes("v i \"").await;
2235        cx.shared_state().await.assert_eq(indoc! {
2236            "hello \"«worldˇ»\"!"
2237        });
2238
2239        cx.set_shared_state(indoc! {
2240            "hello \"wˇorld\"!"
2241        })
2242        .await;
2243        cx.simulate_shared_keystrokes("v a \"").await;
2244        cx.shared_state().await.assert_eq(indoc! {
2245            "hello« \"world\"ˇ»!"
2246        });
2247
2248        cx.set_shared_state(indoc! {
2249            "hello \"wˇorld\" !"
2250        })
2251        .await;
2252        cx.simulate_shared_keystrokes("v a \"").await;
2253        cx.shared_state().await.assert_eq(indoc! {
2254            "hello «\"world\" ˇ»!"
2255        });
2256
2257        cx.set_shared_state(indoc! {
2258            "hello \"wˇorld\"2259            goodbye"
2260        })
2261        .await;
2262        cx.simulate_shared_keystrokes("v a \"").await;
2263        cx.shared_state().await.assert_eq(indoc! {
2264            "hello «\"world\" ˇ»
2265            goodbye"
2266        });
2267    }
2268
2269    #[gpui::test]
2270    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2271        let mut cx = VimTestContext::new(cx, true).await;
2272
2273        cx.set_state(
2274            indoc! {
2275                "func empty(a string) bool {
2276                   if a == \"\" {
2277                      return true
2278                   }
2279                   ˇreturn false
2280                }"
2281            },
2282            Mode::Normal,
2283        );
2284        cx.simulate_keystrokes("v i {");
2285        cx.assert_state(
2286            indoc! {
2287                "func empty(a string) bool {
2288                   «if a == \"\" {
2289                      return true
2290                   }
2291                   return falseˇ»
2292                }"
2293            },
2294            Mode::Visual,
2295        );
2296
2297        cx.set_state(
2298            indoc! {
2299                "func empty(a string) bool {
2300                     if a == \"\" {
2301                         ˇreturn true
2302                     }
2303                     return false
2304                }"
2305            },
2306            Mode::Normal,
2307        );
2308        cx.simulate_keystrokes("v i {");
2309        cx.assert_state(
2310            indoc! {
2311                "func empty(a string) bool {
2312                     if a == \"\" {
2313                         «return trueˇ»
2314                     }
2315                     return false
2316                }"
2317            },
2318            Mode::Visual,
2319        );
2320
2321        cx.set_state(
2322            indoc! {
2323                "func empty(a string) bool {
2324                     if a == \"\" ˇ{
2325                         return true
2326                     }
2327                     return false
2328                }"
2329            },
2330            Mode::Normal,
2331        );
2332        cx.simulate_keystrokes("v i {");
2333        cx.assert_state(
2334            indoc! {
2335                "func empty(a string) bool {
2336                     if a == \"\" {
2337                         «return trueˇ»
2338                     }
2339                     return false
2340                }"
2341            },
2342            Mode::Visual,
2343        );
2344
2345        cx.set_state(
2346            indoc! {
2347                "func empty(a string) bool {
2348                     if a == \"\" {
2349                         return true
2350                     }
2351                     return false
2352                ˇ}"
2353            },
2354            Mode::Normal,
2355        );
2356        cx.simulate_keystrokes("v i {");
2357        cx.assert_state(
2358            indoc! {
2359                "func empty(a string) bool {
2360                     «if a == \"\" {
2361                         return true
2362                     }
2363                     return falseˇ»
2364                }"
2365            },
2366            Mode::Visual,
2367        );
2368
2369        cx.set_state(
2370            indoc! {
2371                "func empty(a string) bool {
2372                             if a == \"\" {
2373                             ˇ
2374
2375                             }"
2376            },
2377            Mode::Normal,
2378        );
2379        cx.simulate_keystrokes("c i {");
2380        cx.assert_state(
2381            indoc! {
2382                "func empty(a string) bool {
2383                             if a == \"\" {ˇ}"
2384            },
2385            Mode::Insert,
2386        );
2387    }
2388
2389    #[gpui::test]
2390    async fn test_singleline_surrounding_character_objects_with_escape(
2391        cx: &mut gpui::TestAppContext,
2392    ) {
2393        let mut cx = NeovimBackedTestContext::new(cx).await;
2394        cx.set_shared_state(indoc! {
2395            "h\"e\\\"lˇlo \\\"world\"!"
2396        })
2397        .await;
2398        cx.simulate_shared_keystrokes("v i \"").await;
2399        cx.shared_state().await.assert_eq(indoc! {
2400            "h\"«e\\\"llo \\\"worldˇ»\"!"
2401        });
2402
2403        cx.set_shared_state(indoc! {
2404            "hello \"teˇst \\\"inside\\\" world\""
2405        })
2406        .await;
2407        cx.simulate_shared_keystrokes("v i \"").await;
2408        cx.shared_state().await.assert_eq(indoc! {
2409            "hello \"«test \\\"inside\\\" worldˇ»\""
2410        });
2411    }
2412
2413    #[gpui::test]
2414    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
2415        let mut cx = VimTestContext::new(cx, true).await;
2416        cx.set_state(
2417            indoc! {"
2418            fn boop() {
2419                baz(ˇ|a, b| { bar(|j, k| { })})
2420            }"
2421            },
2422            Mode::Normal,
2423        );
2424        cx.simulate_keystrokes("c i |");
2425        cx.assert_state(
2426            indoc! {"
2427            fn boop() {
2428                baz(|ˇ| { bar(|j, k| { })})
2429            }"
2430            },
2431            Mode::Insert,
2432        );
2433        cx.simulate_keystrokes("escape 1 8 |");
2434        cx.assert_state(
2435            indoc! {"
2436            fn boop() {
2437                baz(|| { bar(ˇ|j, k| { })})
2438            }"
2439            },
2440            Mode::Normal,
2441        );
2442
2443        cx.simulate_keystrokes("v a |");
2444        cx.assert_state(
2445            indoc! {"
2446            fn boop() {
2447                baz(|| { bar(«|j, k| ˇ»{ })})
2448            }"
2449            },
2450            Mode::Visual,
2451        );
2452    }
2453
2454    #[gpui::test]
2455    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
2456        let mut cx = VimTestContext::new(cx, true).await;
2457
2458        // Generic arguments
2459        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
2460        cx.simulate_keystrokes("v i a");
2461        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
2462
2463        // Function arguments
2464        cx.set_state(
2465            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
2466            Mode::Normal,
2467        );
2468        cx.simulate_keystrokes("d a a");
2469        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
2470
2471        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
2472        cx.simulate_keystrokes("v a a");
2473        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
2474
2475        // Tuple, vec, and array arguments
2476        cx.set_state(
2477            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
2478            Mode::Normal,
2479        );
2480        cx.simulate_keystrokes("c i a");
2481        cx.assert_state(
2482            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
2483            Mode::Insert,
2484        );
2485
2486        // TODO regressed with the up-to-date Rust grammar.
2487        // cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
2488        // cx.simulate_keystrokes("c a a");
2489        // cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
2490
2491        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
2492        cx.simulate_keystrokes("c i a");
2493        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
2494
2495        cx.set_state(
2496            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
2497            Mode::Normal,
2498        );
2499        cx.simulate_keystrokes("c a a");
2500        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
2501
2502        // Cursor immediately before / after brackets
2503        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
2504        cx.simulate_keystrokes("v i a");
2505        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2506
2507        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
2508        cx.simulate_keystrokes("v i a");
2509        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
2510    }
2511
2512    #[gpui::test]
2513    async fn test_indent_object(cx: &mut gpui::TestAppContext) {
2514        let mut cx = VimTestContext::new(cx, true).await;
2515
2516        // Base use case
2517        cx.set_state(
2518            indoc! {"
2519                fn boop() {
2520                    // Comment
2521                    baz();ˇ
2522
2523                    loop {
2524                        bar(1);
2525                        bar(2);
2526                    }
2527
2528                    result
2529                }
2530            "},
2531            Mode::Normal,
2532        );
2533        cx.simulate_keystrokes("v i i");
2534        cx.assert_state(
2535            indoc! {"
2536                fn boop() {
2537                «    // Comment
2538                    baz();
2539
2540                    loop {
2541                        bar(1);
2542                        bar(2);
2543                    }
2544
2545                    resultˇ»
2546                }
2547            "},
2548            Mode::Visual,
2549        );
2550
2551        // Around indent (include line above)
2552        cx.set_state(
2553            indoc! {"
2554                const ABOVE: str = true;
2555                fn boop() {
2556
2557                    hello();
2558                    worˇld()
2559                }
2560            "},
2561            Mode::Normal,
2562        );
2563        cx.simulate_keystrokes("v a i");
2564        cx.assert_state(
2565            indoc! {"
2566                const ABOVE: str = true;
2567                «fn boop() {
2568
2569                    hello();
2570                    world()ˇ»
2571                }
2572            "},
2573            Mode::Visual,
2574        );
2575
2576        // Around indent (include line above & below)
2577        cx.set_state(
2578            indoc! {"
2579                const ABOVE: str = true;
2580                fn boop() {
2581                    hellˇo();
2582                    world()
2583
2584                }
2585                const BELOW: str = true;
2586            "},
2587            Mode::Normal,
2588        );
2589        cx.simulate_keystrokes("c a shift-i");
2590        cx.assert_state(
2591            indoc! {"
2592                const ABOVE: str = true;
2593                ˇ
2594                const BELOW: str = true;
2595            "},
2596            Mode::Insert,
2597        );
2598    }
2599
2600    #[gpui::test]
2601    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
2602        let mut cx = NeovimBackedTestContext::new(cx).await;
2603
2604        for (start, end) in SURROUNDING_OBJECTS {
2605            let marked_string = SURROUNDING_MARKER_STRING
2606                .replace('`', &start.to_string())
2607                .replace('\'', &end.to_string());
2608
2609            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
2610                .await
2611                .assert_matches();
2612            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
2613                .await
2614                .assert_matches();
2615            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
2616                .await
2617                .assert_matches();
2618            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
2619                .await
2620                .assert_matches();
2621        }
2622    }
2623
2624    #[gpui::test]
2625    async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
2626        let mut cx = VimTestContext::new(cx, true).await;
2627        cx.update(|_, cx| {
2628            cx.bind_keys([KeyBinding::new(
2629                "q",
2630                AnyQuotes,
2631                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2632            )]);
2633        });
2634
2635        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2636            // the false string in the middle should be considered
2637            (
2638                "c i q",
2639                "'first' false ˇstring 'second'",
2640                "'first'ˇ'second'",
2641                Mode::Insert,
2642            ),
2643            // Single quotes
2644            (
2645                "c i q",
2646                "Thisˇ is a 'quote' example.",
2647                "This is a 'ˇ' example.",
2648                Mode::Insert,
2649            ),
2650            (
2651                "c a q",
2652                "Thisˇ is a 'quote' example.",
2653                "This is a ˇexample.",
2654                Mode::Insert,
2655            ),
2656            (
2657                "c i q",
2658                "This is a \"simple 'qˇuote'\" example.",
2659                "This is a \"simple 'ˇ'\" example.",
2660                Mode::Insert,
2661            ),
2662            (
2663                "c a q",
2664                "This is a \"simple 'qˇuote'\" example.",
2665                "This is a \"simpleˇ\" example.",
2666                Mode::Insert,
2667            ),
2668            (
2669                "c i q",
2670                "This is a 'qˇuote' example.",
2671                "This is a 'ˇ' example.",
2672                Mode::Insert,
2673            ),
2674            (
2675                "c a q",
2676                "This is a 'qˇuote' example.",
2677                "This is a ˇexample.",
2678                Mode::Insert,
2679            ),
2680            (
2681                "d i q",
2682                "This is a 'qˇuote' example.",
2683                "This is a 'ˇ' example.",
2684                Mode::Normal,
2685            ),
2686            (
2687                "d a q",
2688                "This is a 'qˇuote' example.",
2689                "This is a ˇexample.",
2690                Mode::Normal,
2691            ),
2692            // Double quotes
2693            (
2694                "c i q",
2695                "This is a \"qˇuote\" example.",
2696                "This is a \"ˇ\" example.",
2697                Mode::Insert,
2698            ),
2699            (
2700                "c a q",
2701                "This is a \"qˇuote\" example.",
2702                "This is a ˇexample.",
2703                Mode::Insert,
2704            ),
2705            (
2706                "d i q",
2707                "This is a \"qˇuote\" example.",
2708                "This is a \"ˇ\" example.",
2709                Mode::Normal,
2710            ),
2711            (
2712                "d a q",
2713                "This is a \"qˇuote\" example.",
2714                "This is a ˇexample.",
2715                Mode::Normal,
2716            ),
2717            // Back quotes
2718            (
2719                "c i q",
2720                "This is a `qˇuote` example.",
2721                "This is a `ˇ` example.",
2722                Mode::Insert,
2723            ),
2724            (
2725                "c a q",
2726                "This is a `qˇuote` example.",
2727                "This is a ˇexample.",
2728                Mode::Insert,
2729            ),
2730            (
2731                "d i q",
2732                "This is a `qˇuote` example.",
2733                "This is a `ˇ` example.",
2734                Mode::Normal,
2735            ),
2736            (
2737                "d a q",
2738                "This is a `qˇuote` example.",
2739                "This is a ˇexample.",
2740                Mode::Normal,
2741            ),
2742        ];
2743
2744        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2745            cx.set_state(initial_state, Mode::Normal);
2746
2747            cx.simulate_keystrokes(keystrokes);
2748
2749            cx.assert_state(expected_state, *expected_mode);
2750        }
2751
2752        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2753            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2754            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2755            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2756            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2757            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2758            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2759            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2760            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2761            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2762            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2763            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2764            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2765        ];
2766
2767        for (keystrokes, initial_state, mode) in INVALID_CASES {
2768            cx.set_state(initial_state, Mode::Normal);
2769
2770            cx.simulate_keystrokes(keystrokes);
2771
2772            cx.assert_state(initial_state, *mode);
2773        }
2774    }
2775
2776    #[gpui::test]
2777    async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
2778        let mut cx = VimTestContext::new_typescript(cx).await;
2779
2780        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2781            // Special cases from mini.ai plugin
2782            // the false string in the middle should not be considered
2783            (
2784                "c i q",
2785                "'first' false ˇstring 'second'",
2786                "'first' false string 'ˇ'",
2787                Mode::Insert,
2788            ),
2789            // Multiline support :)! Same behavior as mini.ai plugin
2790            (
2791                "c i q",
2792                indoc! {"
2793                    `
2794                    first
2795                    middle ˇstring
2796                    second
2797                    `
2798                "},
2799                indoc! {"
2800                    `ˇ`
2801                "},
2802                Mode::Insert,
2803            ),
2804            // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
2805            // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
2806            // Bug reference: https://github.com/zed-industries/zed/issues/23889
2807            ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
2808            // Single quotes
2809            (
2810                "c i q",
2811                "Thisˇ is a 'quote' example.",
2812                "This is a 'ˇ' example.",
2813                Mode::Insert,
2814            ),
2815            (
2816                "c a q",
2817                "Thisˇ is a 'quote' example.",
2818                "This is a ˇ example.", // same mini.ai plugin behavior
2819                Mode::Insert,
2820            ),
2821            (
2822                "c i q",
2823                "This is a \"simple 'qˇuote'\" example.",
2824                "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
2825                Mode::Insert,
2826            ),
2827            (
2828                "c a q",
2829                "This is a \"simple 'qˇuote'\" example.",
2830                "This is a ˇ example.", // Not supported by Tree-sitter queries for now
2831                Mode::Insert,
2832            ),
2833            (
2834                "c i q",
2835                "This is a 'qˇuote' example.",
2836                "This is a 'ˇ' example.",
2837                Mode::Insert,
2838            ),
2839            (
2840                "c a q",
2841                "This is a 'qˇuote' example.",
2842                "This is a ˇ example.", // same mini.ai plugin behavior
2843                Mode::Insert,
2844            ),
2845            (
2846                "d i q",
2847                "This is a 'qˇuote' example.",
2848                "This is a 'ˇ' example.",
2849                Mode::Normal,
2850            ),
2851            (
2852                "d a q",
2853                "This is a 'qˇuote' example.",
2854                "This is a ˇ example.", // same mini.ai plugin behavior
2855                Mode::Normal,
2856            ),
2857            // Double quotes
2858            (
2859                "c i q",
2860                "This is a \"qˇuote\" example.",
2861                "This is a \"ˇ\" example.",
2862                Mode::Insert,
2863            ),
2864            (
2865                "c a q",
2866                "This is a \"qˇuote\" example.",
2867                "This is a ˇ example.", // same mini.ai plugin behavior
2868                Mode::Insert,
2869            ),
2870            (
2871                "d i q",
2872                "This is a \"qˇuote\" example.",
2873                "This is a \"ˇ\" example.",
2874                Mode::Normal,
2875            ),
2876            (
2877                "d a q",
2878                "This is a \"qˇuote\" example.",
2879                "This is a ˇ example.", // same mini.ai plugin behavior
2880                Mode::Normal,
2881            ),
2882            // Back quotes
2883            (
2884                "c i q",
2885                "This is a `qˇuote` example.",
2886                "This is a `ˇ` example.",
2887                Mode::Insert,
2888            ),
2889            (
2890                "c a q",
2891                "This is a `qˇuote` example.",
2892                "This is a ˇ example.", // same mini.ai plugin behavior
2893                Mode::Insert,
2894            ),
2895            (
2896                "d i q",
2897                "This is a `qˇuote` example.",
2898                "This is a `ˇ` example.",
2899                Mode::Normal,
2900            ),
2901            (
2902                "d a q",
2903                "This is a `qˇuote` example.",
2904                "This is a ˇ example.", // same mini.ai plugin behavior
2905                Mode::Normal,
2906            ),
2907        ];
2908
2909        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2910            cx.set_state(initial_state, Mode::Normal);
2911            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2912            cx.simulate_keystrokes(keystrokes);
2913            cx.assert_state(expected_state, *expected_mode);
2914        }
2915
2916        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2917            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2918            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2919            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2920            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2921            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2922            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2923            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2924            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2925            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2926            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2927            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2928            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2929        ];
2930
2931        for (keystrokes, initial_state, mode) in INVALID_CASES {
2932            cx.set_state(initial_state, Mode::Normal);
2933            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
2934            cx.simulate_keystrokes(keystrokes);
2935            cx.assert_state(initial_state, *mode);
2936        }
2937    }
2938
2939    #[gpui::test]
2940    async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2941        let mut cx = VimTestContext::new(cx, true).await;
2942        cx.update(|_, cx| {
2943            cx.bind_keys([KeyBinding::new(
2944                "b",
2945                AnyBrackets,
2946                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2947            )]);
2948        });
2949
2950        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2951            (
2952                "c i b",
2953                indoc! {"
2954                    {
2955                        {
2956                            ˇprint('hello')
2957                        }
2958                    }
2959                "},
2960                indoc! {"
2961                    {
2962                        {
2963                            ˇ
2964                        }
2965                    }
2966                "},
2967                Mode::Insert,
2968            ),
2969            // Bracket (Parentheses)
2970            (
2971                "c i b",
2972                "Thisˇ is a (simple [quote]) example.",
2973                "This is a (ˇ) example.",
2974                Mode::Insert,
2975            ),
2976            (
2977                "c i b",
2978                "This is a [simple (qˇuote)] example.",
2979                "This is a [simple (ˇ)] example.",
2980                Mode::Insert,
2981            ),
2982            (
2983                "c a b",
2984                "This is a [simple (qˇuote)] example.",
2985                "This is a [simple ˇ] example.",
2986                Mode::Insert,
2987            ),
2988            (
2989                "c a b",
2990                "Thisˇ is a (simple [quote]) example.",
2991                "This is a ˇ example.",
2992                Mode::Insert,
2993            ),
2994            (
2995                "c i b",
2996                "This is a (qˇuote) example.",
2997                "This is a (ˇ) example.",
2998                Mode::Insert,
2999            ),
3000            (
3001                "c a b",
3002                "This is a (qˇuote) example.",
3003                "This is a ˇ example.",
3004                Mode::Insert,
3005            ),
3006            (
3007                "d i b",
3008                "This is a (qˇuote) example.",
3009                "This is a (ˇ) example.",
3010                Mode::Normal,
3011            ),
3012            (
3013                "d a b",
3014                "This is a (qˇuote) example.",
3015                "This is a ˇ example.",
3016                Mode::Normal,
3017            ),
3018            // Square brackets
3019            (
3020                "c i b",
3021                "This is a [qˇuote] example.",
3022                "This is a [ˇ] example.",
3023                Mode::Insert,
3024            ),
3025            (
3026                "c a b",
3027                "This is a [qˇuote] example.",
3028                "This is a ˇ example.",
3029                Mode::Insert,
3030            ),
3031            (
3032                "d i b",
3033                "This is a [qˇuote] example.",
3034                "This is a [ˇ] example.",
3035                Mode::Normal,
3036            ),
3037            (
3038                "d a b",
3039                "This is a [qˇuote] example.",
3040                "This is a ˇ example.",
3041                Mode::Normal,
3042            ),
3043            // Curly brackets
3044            (
3045                "c i b",
3046                "This is a {qˇuote} example.",
3047                "This is a {ˇ} example.",
3048                Mode::Insert,
3049            ),
3050            (
3051                "c a b",
3052                "This is a {qˇuote} example.",
3053                "This is a ˇ example.",
3054                Mode::Insert,
3055            ),
3056            (
3057                "d i b",
3058                "This is a {qˇuote} example.",
3059                "This is a {ˇ} example.",
3060                Mode::Normal,
3061            ),
3062            (
3063                "d a b",
3064                "This is a {qˇuote} example.",
3065                "This is a ˇ example.",
3066                Mode::Normal,
3067            ),
3068        ];
3069
3070        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3071            cx.set_state(initial_state, Mode::Normal);
3072
3073            cx.simulate_keystrokes(keystrokes);
3074
3075            cx.assert_state(expected_state, *expected_mode);
3076        }
3077
3078        const INVALID_CASES: &[(&str, &str, Mode)] = &[
3079            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3080            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3081            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3082            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3083            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3084            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3085            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3086            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3087            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3088            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3089            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3090            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3091        ];
3092
3093        for (keystrokes, initial_state, mode) in INVALID_CASES {
3094            cx.set_state(initial_state, Mode::Normal);
3095
3096            cx.simulate_keystrokes(keystrokes);
3097
3098            cx.assert_state(initial_state, *mode);
3099        }
3100    }
3101
3102    #[gpui::test]
3103    async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
3104        let mut cx = VimTestContext::new(cx, true).await;
3105        cx.update(|_, cx| {
3106            cx.bind_keys([KeyBinding::new(
3107                "b",
3108                MiniBrackets,
3109                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
3110            )]);
3111        });
3112
3113        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
3114            // Special cases from mini.ai plugin
3115            // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
3116            // Same behavior as mini.ai plugin
3117            (
3118                "c i b",
3119                indoc! {"
3120                    {
3121                        {
3122                            ˇprint('hello')
3123                        }
3124                    }
3125                "},
3126                indoc! {"
3127                    {
3128                        {
3129                            print(ˇ)
3130                        }
3131                    }
3132                "},
3133                Mode::Insert,
3134            ),
3135            // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
3136            // Same behavior as mini.ai plugin
3137            (
3138                "c i b",
3139                indoc! {"
3140                    {
3141                        {
3142                            ˇ
3143                            print('hello')
3144                        }
3145                    }
3146                "},
3147                indoc! {"
3148                    {
3149                        {ˇ}
3150                    }
3151                "},
3152                Mode::Insert,
3153            ),
3154            // If you are in the open bracket then it has higher priority
3155            (
3156                "c i b",
3157                indoc! {"
3158                    «{ˇ»
3159                        {
3160                            print('hello')
3161                        }
3162                    }
3163                "},
3164                indoc! {"
3165                    {ˇ}
3166                "},
3167                Mode::Insert,
3168            ),
3169            // If you are in the close bracket then it has higher priority
3170            (
3171                "c i b",
3172                indoc! {"
3173                    {
3174                        {
3175                            print('hello')
3176                        }
3177                    «}ˇ»
3178                "},
3179                indoc! {"
3180                    {ˇ}
3181                "},
3182                Mode::Insert,
3183            ),
3184            // Bracket (Parentheses)
3185            (
3186                "c i b",
3187                "Thisˇ is a (simple [quote]) example.",
3188                "This is a (ˇ) example.",
3189                Mode::Insert,
3190            ),
3191            (
3192                "c i b",
3193                "This is a [simple (qˇuote)] example.",
3194                "This is a [simple (ˇ)] example.",
3195                Mode::Insert,
3196            ),
3197            (
3198                "c a b",
3199                "This is a [simple (qˇuote)] example.",
3200                "This is a [simple ˇ] example.",
3201                Mode::Insert,
3202            ),
3203            (
3204                "c a b",
3205                "Thisˇ is a (simple [quote]) example.",
3206                "This is a ˇ example.",
3207                Mode::Insert,
3208            ),
3209            (
3210                "c i b",
3211                "This is a (qˇuote) example.",
3212                "This is a (ˇ) example.",
3213                Mode::Insert,
3214            ),
3215            (
3216                "c a b",
3217                "This is a (qˇuote) example.",
3218                "This is a ˇ example.",
3219                Mode::Insert,
3220            ),
3221            (
3222                "d i b",
3223                "This is a (qˇuote) example.",
3224                "This is a (ˇ) example.",
3225                Mode::Normal,
3226            ),
3227            (
3228                "d a b",
3229                "This is a (qˇuote) example.",
3230                "This is a ˇ example.",
3231                Mode::Normal,
3232            ),
3233            // Square brackets
3234            (
3235                "c i b",
3236                "This is a [qˇuote] example.",
3237                "This is a [ˇ] example.",
3238                Mode::Insert,
3239            ),
3240            (
3241                "c a b",
3242                "This is a [qˇuote] example.",
3243                "This is a ˇ example.",
3244                Mode::Insert,
3245            ),
3246            (
3247                "d i b",
3248                "This is a [qˇuote] example.",
3249                "This is a [ˇ] example.",
3250                Mode::Normal,
3251            ),
3252            (
3253                "d a b",
3254                "This is a [qˇuote] example.",
3255                "This is a ˇ example.",
3256                Mode::Normal,
3257            ),
3258            // Curly brackets
3259            (
3260                "c i b",
3261                "This is a {qˇuote} example.",
3262                "This is a {ˇ} example.",
3263                Mode::Insert,
3264            ),
3265            (
3266                "c a b",
3267                "This is a {qˇuote} example.",
3268                "This is a ˇ example.",
3269                Mode::Insert,
3270            ),
3271            (
3272                "d i b",
3273                "This is a {qˇuote} example.",
3274                "This is a {ˇ} example.",
3275                Mode::Normal,
3276            ),
3277            (
3278                "d a b",
3279                "This is a {qˇuote} example.",
3280                "This is a ˇ example.",
3281                Mode::Normal,
3282            ),
3283        ];
3284
3285        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
3286            cx.set_state(initial_state, Mode::Normal);
3287            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3288            cx.simulate_keystrokes(keystrokes);
3289            cx.assert_state(expected_state, *expected_mode);
3290        }
3291
3292        const INVALID_CASES: &[(&str, &str, Mode)] = &[
3293            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3294            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3295            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3296            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
3297            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3298            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3299            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3300            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
3301            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3302            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3303            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3304            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
3305        ];
3306
3307        for (keystrokes, initial_state, mode) in INVALID_CASES {
3308            cx.set_state(initial_state, Mode::Normal);
3309            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
3310            cx.simulate_keystrokes(keystrokes);
3311            cx.assert_state(initial_state, *mode);
3312        }
3313    }
3314
3315    #[gpui::test]
3316    async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) {
3317        // Initialize test context with the TypeScript language loaded, so we
3318        // can actually get brackets definition.
3319        let mut cx = VimTestContext::new(cx, true).await;
3320
3321        // Update `b` to `MiniBrackets` so we can later use it when simulating
3322        // keystrokes.
3323        cx.update(|_, cx| {
3324            cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]);
3325        });
3326
3327        let (editor, cx) = cx.add_window_view(|window, cx| {
3328            let multi_buffer = MultiBuffer::build_multi(
3329                [
3330                    ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
3331                    ("111\na {bracket} example\n", vec![Point::row_range(0..2)]),
3332                ],
3333                cx,
3334            );
3335
3336            // In order for the brackets to actually be found, we need to update
3337            // the language used for the second buffer. This is something that
3338            // is handled automatically when simply using `VimTestContext::new`
3339            // but, since this is being set manually, the language isn't
3340            // automatically set.
3341            let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
3342            let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
3343            if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) {
3344                buffer.update(cx, |buffer, cx| {
3345                    buffer.set_language(Some(language::rust_lang()), cx);
3346                })
3347            };
3348
3349            editor
3350        });
3351
3352        let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
3353
3354        cx.assert_excerpts_with_selections(indoc! {"
3355            [EXCERPT]
3356            ˇ111
3357            222
3358            [EXCERPT]
3359            111
3360            a {bracket} example
3361            "
3362        });
3363
3364        cx.simulate_keystrokes("j j j j f r");
3365        cx.assert_excerpts_with_selections(indoc! {"
3366            [EXCERPT]
3367            111
3368            222
3369            [EXCERPT]
3370            111
3371            a {bˇracket} example
3372            "
3373        });
3374
3375        cx.simulate_keystrokes("d i b");
3376        cx.assert_excerpts_with_selections(indoc! {"
3377            [EXCERPT]
3378            111
3379            222
3380            [EXCERPT]
3381            111
3382            a {ˇ} example
3383            "
3384        });
3385    }
3386
3387    #[gpui::test]
3388    async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
3389        let mut cx = NeovimBackedTestContext::new(cx).await;
3390        cx.set_shared_state("(trailingˇ whitespace          )")
3391            .await;
3392        cx.simulate_shared_keystrokes("v i b").await;
3393        cx.shared_state().await.assert_matches();
3394        cx.simulate_shared_keystrokes("escape y i b").await;
3395        cx.shared_clipboard()
3396            .await
3397            .assert_eq("trailing whitespace          ");
3398    }
3399
3400    #[gpui::test]
3401    async fn test_tags(cx: &mut gpui::TestAppContext) {
3402        let mut cx = VimTestContext::new_html(cx).await;
3403
3404        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
3405        cx.simulate_keystrokes("v i t");
3406        cx.assert_state(
3407            "<html><head></head><body><b>«hi!ˇ»</b></body>",
3408            Mode::Visual,
3409        );
3410        cx.simulate_keystrokes("a t");
3411        cx.assert_state(
3412            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3413            Mode::Visual,
3414        );
3415        cx.simulate_keystrokes("a t");
3416        cx.assert_state(
3417            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3418            Mode::Visual,
3419        );
3420
3421        // The cursor is before the tag
3422        cx.set_state(
3423            "<html><head></head><body> ˇ  <b>hi!</b></body>",
3424            Mode::Normal,
3425        );
3426        cx.simulate_keystrokes("v i t");
3427        cx.assert_state(
3428            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
3429            Mode::Visual,
3430        );
3431        cx.simulate_keystrokes("a t");
3432        cx.assert_state(
3433            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
3434            Mode::Visual,
3435        );
3436
3437        // The cursor is in the open tag
3438        cx.set_state(
3439            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
3440            Mode::Normal,
3441        );
3442        cx.simulate_keystrokes("v a t");
3443        cx.assert_state(
3444            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
3445            Mode::Visual,
3446        );
3447        cx.simulate_keystrokes("i t");
3448        cx.assert_state(
3449            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
3450            Mode::Visual,
3451        );
3452
3453        // current selection length greater than 1
3454        cx.set_state(
3455            "<html><head></head><body><«b>hi!ˇ»</b></body>",
3456            Mode::Visual,
3457        );
3458        cx.simulate_keystrokes("i t");
3459        cx.assert_state(
3460            "<html><head></head><body><b>«hi!ˇ»</b></body>",
3461            Mode::Visual,
3462        );
3463        cx.simulate_keystrokes("a t");
3464        cx.assert_state(
3465            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
3466            Mode::Visual,
3467        );
3468
3469        cx.set_state(
3470            "<html><head></head><body><«b>hi!</ˇ»b></body>",
3471            Mode::Visual,
3472        );
3473        cx.simulate_keystrokes("a t");
3474        cx.assert_state(
3475            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
3476            Mode::Visual,
3477        );
3478    }
3479    #[gpui::test]
3480    async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
3481        let mut cx = NeovimBackedTestContext::new(cx).await;
3482
3483        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3484            .await;
3485        cx.simulate_shared_keystrokes("v a w").await;
3486        cx.shared_state()
3487            .await
3488            .assert_eq("    «const ˇ»f = (x: unknown) => {");
3489
3490        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3491            .await;
3492        cx.simulate_shared_keystrokes("y a w").await;
3493        cx.shared_clipboard().await.assert_eq("const ");
3494
3495        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3496            .await;
3497        cx.simulate_shared_keystrokes("d a w").await;
3498        cx.shared_state()
3499            .await
3500            .assert_eq("    ˇf = (x: unknown) => {");
3501        cx.shared_clipboard().await.assert_eq("const ");
3502
3503        cx.set_shared_state("    ˇconst f = (x: unknown) => {")
3504            .await;
3505        cx.simulate_shared_keystrokes("c a w").await;
3506        cx.shared_state()
3507            .await
3508            .assert_eq("    ˇf = (x: unknown) => {");
3509        cx.shared_clipboard().await.assert_eq("const ");
3510    }
3511
3512    #[gpui::test]
3513    async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
3514        let mut cx = VimTestContext::new_typescript(cx).await;
3515
3516        cx.set_state(
3517            indoc! {"
3518                const foo = () => {
3519                    return ˇ1;
3520                };
3521            "},
3522            Mode::Normal,
3523        );
3524        cx.simulate_keystrokes("v a f");
3525        cx.assert_state(
3526            indoc! {"
3527                «const foo = () => {
3528                    return 1;
3529                };ˇ»
3530            "},
3531            Mode::VisualLine,
3532        );
3533
3534        cx.set_state(
3535            indoc! {"
3536                arr.map(() => {
3537                    return ˇ1;
3538                });
3539            "},
3540            Mode::Normal,
3541        );
3542        cx.simulate_keystrokes("v a f");
3543        cx.assert_state(
3544            indoc! {"
3545                arr.map(«() => {
3546                    return 1;
3547                }ˇ»);
3548            "},
3549            Mode::VisualLine,
3550        );
3551
3552        cx.set_state(
3553            indoc! {"
3554                const foo = () => {
3555                    return ˇ1;
3556                };
3557            "},
3558            Mode::Normal,
3559        );
3560        cx.simulate_keystrokes("v i f");
3561        cx.assert_state(
3562            indoc! {"
3563                const foo = () => {
3564                    «return 1;ˇ»
3565                };
3566            "},
3567            Mode::Visual,
3568        );
3569
3570        cx.set_state(
3571            indoc! {"
3572                (() => {
3573                    console.log(ˇ1);
3574                })();
3575            "},
3576            Mode::Normal,
3577        );
3578        cx.simulate_keystrokes("v a f");
3579        cx.assert_state(
3580            indoc! {"
3581                («() => {
3582                    console.log(1);
3583                }ˇ»)();
3584            "},
3585            Mode::VisualLine,
3586        );
3587
3588        cx.set_state(
3589            indoc! {"
3590                const foo = () => {
3591                    return ˇ1;
3592                };
3593                export { foo };
3594            "},
3595            Mode::Normal,
3596        );
3597        cx.simulate_keystrokes("v a f");
3598        cx.assert_state(
3599            indoc! {"
3600                «const foo = () => {
3601                    return 1;
3602                };ˇ»
3603                export { foo };
3604            "},
3605            Mode::VisualLine,
3606        );
3607
3608        cx.set_state(
3609            indoc! {"
3610                let bar = () => {
3611                    return ˇ2;
3612                };
3613            "},
3614            Mode::Normal,
3615        );
3616        cx.simulate_keystrokes("v a f");
3617        cx.assert_state(
3618            indoc! {"
3619                «let bar = () => {
3620                    return 2;
3621                };ˇ»
3622            "},
3623            Mode::VisualLine,
3624        );
3625
3626        cx.set_state(
3627            indoc! {"
3628                var baz = () => {
3629                    return ˇ3;
3630                };
3631            "},
3632            Mode::Normal,
3633        );
3634        cx.simulate_keystrokes("v a f");
3635        cx.assert_state(
3636            indoc! {"
3637                «var baz = () => {
3638                    return 3;
3639                };ˇ»
3640            "},
3641            Mode::VisualLine,
3642        );
3643
3644        cx.set_state(
3645            indoc! {"
3646                const add = (a, b) => a + ˇb;
3647            "},
3648            Mode::Normal,
3649        );
3650        cx.simulate_keystrokes("v a f");
3651        cx.assert_state(
3652            indoc! {"
3653                «const add = (a, b) => a + b;ˇ»
3654            "},
3655            Mode::VisualLine,
3656        );
3657
3658        cx.set_state(
3659            indoc! {"
3660                const add = ˇ(a, b) => a + b;
3661            "},
3662            Mode::Normal,
3663        );
3664        cx.simulate_keystrokes("v a f");
3665        cx.assert_state(
3666            indoc! {"
3667                «const add = (a, b) => a + b;ˇ»
3668            "},
3669            Mode::VisualLine,
3670        );
3671
3672        cx.set_state(
3673            indoc! {"
3674                const add = (a, b) => a + bˇ;
3675            "},
3676            Mode::Normal,
3677        );
3678        cx.simulate_keystrokes("v a f");
3679        cx.assert_state(
3680            indoc! {"
3681                «const add = (a, b) => a + b;ˇ»
3682            "},
3683            Mode::VisualLine,
3684        );
3685
3686        cx.set_state(
3687            indoc! {"
3688                const add = (a, b) =ˇ> a + b;
3689            "},
3690            Mode::Normal,
3691        );
3692        cx.simulate_keystrokes("v a f");
3693        cx.assert_state(
3694            indoc! {"
3695                «const add = (a, b) => a + b;ˇ»
3696            "},
3697            Mode::VisualLine,
3698        );
3699    }
3700
3701    #[gpui::test]
3702    async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
3703        let mut cx = VimTestContext::new_tsx(cx).await;
3704
3705        cx.set_state(
3706            indoc! {r#"
3707                export const MyComponent = () => {
3708                  return (
3709                    <div>
3710                      <div onClick={() => {
3711                        alert("Hello world!");
3712                        console.log(ˇ"clicked");
3713                      }}>Hello world!</div>
3714                    </div>
3715                  );
3716                };
3717            "#},
3718            Mode::Normal,
3719        );
3720        cx.simulate_keystrokes("v a f");
3721        cx.assert_state(
3722            indoc! {r#"
3723                export const MyComponent = () => {
3724                  return (
3725                    <div>
3726                      <div onClick={«() => {
3727                        alert("Hello world!");
3728                        console.log("clicked");
3729                      }ˇ»}>Hello world!</div>
3730                    </div>
3731                  );
3732                };
3733            "#},
3734            Mode::VisualLine,
3735        );
3736
3737        cx.set_state(
3738            indoc! {r#"
3739                export const MyComponent = () => {
3740                  return (
3741                    <div>
3742                      <div onClick={() => console.log("clickˇed")}>Hello world!</div>
3743                    </div>
3744                  );
3745                };
3746            "#},
3747            Mode::Normal,
3748        );
3749        cx.simulate_keystrokes("v a f");
3750        cx.assert_state(
3751            indoc! {r#"
3752                export const MyComponent = () => {
3753                  return (
3754                    <div>
3755                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3756                    </div>
3757                  );
3758                };
3759            "#},
3760            Mode::VisualLine,
3761        );
3762
3763        cx.set_state(
3764            indoc! {r#"
3765                export const MyComponent = () => {
3766                  return (
3767                    <div>
3768                      <div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
3769                    </div>
3770                  );
3771                };
3772            "#},
3773            Mode::Normal,
3774        );
3775        cx.simulate_keystrokes("v a f");
3776        cx.assert_state(
3777            indoc! {r#"
3778                export const MyComponent = () => {
3779                  return (
3780                    <div>
3781                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3782                    </div>
3783                  );
3784                };
3785            "#},
3786            Mode::VisualLine,
3787        );
3788
3789        cx.set_state(
3790            indoc! {r#"
3791                export const MyComponent = () => {
3792                  return (
3793                    <div>
3794                      <div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
3795                    </div>
3796                  );
3797                };
3798            "#},
3799            Mode::Normal,
3800        );
3801        cx.simulate_keystrokes("v a f");
3802        cx.assert_state(
3803            indoc! {r#"
3804                export const MyComponent = () => {
3805                  return (
3806                    <div>
3807                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3808                    </div>
3809                  );
3810                };
3811            "#},
3812            Mode::VisualLine,
3813        );
3814
3815        cx.set_state(
3816            indoc! {r#"
3817                export const MyComponent = () => {
3818                  return (
3819                    <div>
3820                      <div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
3821                    </div>
3822                  );
3823                };
3824            "#},
3825            Mode::Normal,
3826        );
3827        cx.simulate_keystrokes("v a f");
3828        cx.assert_state(
3829            indoc! {r#"
3830                export const MyComponent = () => {
3831                  return (
3832                    <div>
3833                      <div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
3834                    </div>
3835                  );
3836                };
3837            "#},
3838            Mode::VisualLine,
3839        );
3840
3841        cx.set_state(
3842            indoc! {r#"
3843                export const MyComponent = () => {
3844                  return (
3845                    <div>
3846                      <div onClick={() => {
3847                        console.log("cliˇcked");
3848                      }}>Hello world!</div>
3849                    </div>
3850                  );
3851                };
3852            "#},
3853            Mode::Normal,
3854        );
3855        cx.simulate_keystrokes("v a f");
3856        cx.assert_state(
3857            indoc! {r#"
3858                export const MyComponent = () => {
3859                  return (
3860                    <div>
3861                      <div onClick={«() => {
3862                        console.log("clicked");
3863                      }ˇ»}>Hello world!</div>
3864                    </div>
3865                  );
3866                };
3867            "#},
3868            Mode::VisualLine,
3869        );
3870
3871        cx.set_state(
3872            indoc! {r#"
3873                export const MyComponent = () => {
3874                  return (
3875                    <div>
3876                      <div onClick={() => fˇoo()}>Hello world!</div>
3877                    </div>
3878                  );
3879                };
3880            "#},
3881            Mode::Normal,
3882        );
3883        cx.simulate_keystrokes("v a f");
3884        cx.assert_state(
3885            indoc! {r#"
3886                export const MyComponent = () => {
3887                  return (
3888                    <div>
3889                      <div onClick={«() => foo()ˇ»}>Hello world!</div>
3890                    </div>
3891                  );
3892                };
3893            "#},
3894            Mode::VisualLine,
3895        );
3896    }
3897
3898    #[gpui::test]
3899    async fn test_subword_object(cx: &mut gpui::TestAppContext) {
3900        let mut cx = VimTestContext::new(cx, true).await;
3901
3902        // Setup custom keybindings for subword object so we can use the
3903        // bindings in `simulate_keystrokes`.
3904        cx.update(|_window, cx| {
3905            cx.bind_keys([KeyBinding::new(
3906                "w",
3907                super::Subword {
3908                    ignore_punctuation: false,
3909                },
3910                Some("vim_operator"),
3911            )]);
3912        });
3913
3914        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3915        cx.simulate_keystrokes("c i w");
3916        cx.assert_state("foo_ˇ_baz", Mode::Insert);
3917
3918        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
3919        cx.simulate_keystrokes("c i w");
3920        cx.assert_state("ˇ_bar_baz", Mode::Insert);
3921
3922        cx.set_state("foo_bar_baˇz", Mode::Normal);
3923        cx.simulate_keystrokes("c i w");
3924        cx.assert_state("foo_bar_ˇ", Mode::Insert);
3925
3926        cx.set_state("fooˇBarBaz", Mode::Normal);
3927        cx.simulate_keystrokes("c i w");
3928        cx.assert_state("fooˇBaz", Mode::Insert);
3929
3930        cx.set_state("ˇfooBarBaz", Mode::Normal);
3931        cx.simulate_keystrokes("c i w");
3932        cx.assert_state("ˇBarBaz", Mode::Insert);
3933
3934        cx.set_state("fooBarBaˇz", Mode::Normal);
3935        cx.simulate_keystrokes("c i w");
3936        cx.assert_state("fooBarˇ", Mode::Insert);
3937
3938        cx.set_state("foo.ˇbar.baz", Mode::Normal);
3939        cx.simulate_keystrokes("c i w");
3940        cx.assert_state("foo.ˇ.baz", Mode::Insert);
3941
3942        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3943        cx.simulate_keystrokes("d i w");
3944        cx.assert_state("foo_ˇ_baz", Mode::Normal);
3945
3946        cx.set_state("fooˇBarBaz", Mode::Normal);
3947        cx.simulate_keystrokes("d i w");
3948        cx.assert_state("fooˇBaz", Mode::Normal);
3949
3950        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3951        cx.simulate_keystrokes("c a w");
3952        cx.assert_state("foo_ˇ_baz", Mode::Insert);
3953
3954        cx.set_state("fooˇBarBaz", Mode::Normal);
3955        cx.simulate_keystrokes("c a w");
3956        cx.assert_state("fooˇBaz", Mode::Insert);
3957
3958        cx.set_state("foo_ˇbar_baz", Mode::Normal);
3959        cx.simulate_keystrokes("d a w");
3960        cx.assert_state("foo_ˇ_baz", Mode::Normal);
3961
3962        cx.set_state("fooˇBarBaz", Mode::Normal);
3963        cx.simulate_keystrokes("d a w");
3964        cx.assert_state("fooˇBaz", Mode::Normal);
3965    }
3966}