object.rs

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