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