object.rs

   1use std::ops::Range;
   2
   3use crate::{
   4    motion::right,
   5    state::{Mode, Operator},
   6    Vim,
   7};
   8use editor::{
   9    display_map::{DisplaySnapshot, ToDisplayPoint},
  10    movement::{self, FindRange},
  11    Bias, DisplayPoint, Editor,
  12};
  13use gpui::{actions, impl_actions, Window};
  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)]
  22pub enum Object {
  23    Word { ignore_punctuation: bool },
  24    Subword { ignore_punctuation: bool },
  25    Sentence,
  26    Paragraph,
  27    Quotes,
  28    BackQuotes,
  29    AnyQuotes,
  30    DoubleQuotes,
  31    VerticalBars,
  32    AnyBrackets,
  33    Parentheses,
  34    SquareBrackets,
  35    CurlyBrackets,
  36    AngleBrackets,
  37    Argument,
  38    IndentObj { include_below: bool },
  39    Tag,
  40    Method,
  41    Class,
  42    Comment,
  43    EntireFile,
  44}
  45
  46#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  47#[serde(rename_all = "camelCase")]
  48struct Word {
  49    #[serde(default)]
  50    ignore_punctuation: bool,
  51}
  52
  53#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  54#[serde(rename_all = "camelCase")]
  55struct Subword {
  56    #[serde(default)]
  57    ignore_punctuation: bool,
  58}
  59#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  60#[serde(rename_all = "camelCase")]
  61struct IndentObj {
  62    #[serde(default)]
  63    include_below: bool,
  64}
  65
  66impl_actions!(vim, [Word, Subword, IndentObj]);
  67
  68actions!(
  69    vim,
  70    [
  71        Sentence,
  72        Paragraph,
  73        Quotes,
  74        BackQuotes,
  75        AnyQuotes,
  76        DoubleQuotes,
  77        VerticalBars,
  78        Parentheses,
  79        AnyBrackets,
  80        SquareBrackets,
  81        CurlyBrackets,
  82        AngleBrackets,
  83        Argument,
  84        Tag,
  85        Method,
  86        Class,
  87        Comment,
  88        EntireFile
  89    ]
  90);
  91
  92pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  93    Vim::action(
  94        editor,
  95        cx,
  96        |vim, &Word { ignore_punctuation }: &Word, window, cx| {
  97            vim.object(Object::Word { ignore_punctuation }, window, cx)
  98        },
  99    );
 100    Vim::action(
 101        editor,
 102        cx,
 103        |vim, &Subword { ignore_punctuation }: &Subword, window, cx| {
 104            vim.object(Object::Subword { ignore_punctuation }, window, cx)
 105        },
 106    );
 107    Vim::action(editor, cx, |vim, _: &Tag, window, cx| {
 108        vim.object(Object::Tag, window, cx)
 109    });
 110    Vim::action(editor, cx, |vim, _: &Sentence, window, cx| {
 111        vim.object(Object::Sentence, window, cx)
 112    });
 113    Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| {
 114        vim.object(Object::Paragraph, window, cx)
 115    });
 116    Vim::action(editor, cx, |vim, _: &Quotes, window, cx| {
 117        vim.object(Object::Quotes, window, cx)
 118    });
 119    Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
 120        vim.object(Object::AnyQuotes, window, cx)
 121    });
 122    Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| {
 123        vim.object(Object::AnyBrackets, window, cx)
 124    });
 125    Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
 126        vim.object(Object::DoubleQuotes, window, cx)
 127    });
 128    Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
 129        vim.object(Object::DoubleQuotes, window, cx)
 130    });
 131    Vim::action(editor, cx, |vim, _: &Parentheses, window, cx| {
 132        vim.object(Object::Parentheses, window, cx)
 133    });
 134    Vim::action(editor, cx, |vim, _: &SquareBrackets, window, cx| {
 135        vim.object(Object::SquareBrackets, window, cx)
 136    });
 137    Vim::action(editor, cx, |vim, _: &CurlyBrackets, window, cx| {
 138        vim.object(Object::CurlyBrackets, window, cx)
 139    });
 140    Vim::action(editor, cx, |vim, _: &AngleBrackets, window, cx| {
 141        vim.object(Object::AngleBrackets, window, cx)
 142    });
 143    Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
 144        vim.object(Object::VerticalBars, window, cx)
 145    });
 146    Vim::action(editor, cx, |vim, _: &Argument, window, cx| {
 147        vim.object(Object::Argument, window, cx)
 148    });
 149    Vim::action(editor, cx, |vim, _: &Method, window, cx| {
 150        vim.object(Object::Method, window, cx)
 151    });
 152    Vim::action(editor, cx, |vim, _: &Class, window, cx| {
 153        vim.object(Object::Class, window, cx)
 154    });
 155    Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| {
 156        vim.object(Object::EntireFile, window, cx)
 157    });
 158    Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
 159        if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
 160            vim.push_operator(Operator::Object { around: true }, window, cx);
 161        }
 162        vim.object(Object::Comment, window, cx)
 163    });
 164    Vim::action(
 165        editor,
 166        cx,
 167        |vim, &IndentObj { include_below }: &IndentObj, window, cx| {
 168            vim.object(Object::IndentObj { include_below }, window, cx)
 169        },
 170    );
 171}
 172
 173impl Vim {
 174    fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
 175        match self.mode {
 176            Mode::Normal => self.normal_object(object, window, cx),
 177            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 178                self.visual_object(object, window, cx)
 179            }
 180            Mode::Insert | Mode::Replace | Mode::HelixNormal => {
 181                // Shouldn't execute a text object in insert mode. Ignoring
 182            }
 183        }
 184    }
 185}
 186
 187impl Object {
 188    pub fn is_multiline(self) -> bool {
 189        match self {
 190            Object::Word { .. }
 191            | Object::Subword { .. }
 192            | Object::Quotes
 193            | Object::BackQuotes
 194            | Object::AnyQuotes
 195            | Object::VerticalBars
 196            | Object::DoubleQuotes => false,
 197            Object::Sentence
 198            | Object::Paragraph
 199            | Object::AnyBrackets
 200            | Object::Parentheses
 201            | Object::Tag
 202            | Object::AngleBrackets
 203            | Object::CurlyBrackets
 204            | Object::SquareBrackets
 205            | Object::Argument
 206            | Object::Method
 207            | Object::Class
 208            | Object::EntireFile
 209            | Object::Comment
 210            | Object::IndentObj { .. } => true,
 211        }
 212    }
 213
 214    pub fn always_expands_both_ways(self) -> bool {
 215        match self {
 216            Object::Word { .. }
 217            | Object::Subword { .. }
 218            | Object::Sentence
 219            | Object::Paragraph
 220            | Object::Argument
 221            | Object::IndentObj { .. } => false,
 222            Object::Quotes
 223            | Object::BackQuotes
 224            | Object::AnyQuotes
 225            | Object::DoubleQuotes
 226            | Object::VerticalBars
 227            | Object::AnyBrackets
 228            | Object::Parentheses
 229            | Object::SquareBrackets
 230            | Object::Tag
 231            | Object::Method
 232            | Object::Class
 233            | Object::Comment
 234            | Object::EntireFile
 235            | Object::CurlyBrackets
 236            | Object::AngleBrackets => true,
 237        }
 238    }
 239
 240    pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
 241        match self {
 242            Object::Word { .. }
 243            | Object::Subword { .. }
 244            | Object::Sentence
 245            | Object::Quotes
 246            | Object::AnyQuotes
 247            | Object::BackQuotes
 248            | Object::DoubleQuotes => {
 249                if current_mode == Mode::VisualBlock {
 250                    Mode::VisualBlock
 251                } else {
 252                    Mode::Visual
 253                }
 254            }
 255            Object::Parentheses
 256            | Object::AnyBrackets
 257            | Object::SquareBrackets
 258            | Object::CurlyBrackets
 259            | Object::AngleBrackets
 260            | Object::VerticalBars
 261            | Object::Tag
 262            | Object::Comment
 263            | Object::Argument
 264            | Object::IndentObj { .. } => Mode::Visual,
 265            Object::Method | Object::Class => {
 266                if around {
 267                    Mode::VisualLine
 268                } else {
 269                    Mode::Visual
 270                }
 271            }
 272            Object::Paragraph | Object::EntireFile => Mode::VisualLine,
 273        }
 274    }
 275
 276    pub fn range(
 277        self,
 278        map: &DisplaySnapshot,
 279        selection: Selection<DisplayPoint>,
 280        around: bool,
 281    ) -> Option<Range<DisplayPoint>> {
 282        let relative_to = selection.head();
 283        match self {
 284            Object::Word { ignore_punctuation } => {
 285                if around {
 286                    around_word(map, relative_to, ignore_punctuation)
 287                } else {
 288                    in_word(map, relative_to, ignore_punctuation)
 289                }
 290            }
 291            Object::Subword { ignore_punctuation } => {
 292                if around {
 293                    around_subword(map, relative_to, ignore_punctuation)
 294                } else {
 295                    in_subword(map, relative_to, ignore_punctuation)
 296                }
 297            }
 298            Object::Sentence => sentence(map, relative_to, around),
 299            Object::Paragraph => paragraph(map, relative_to, around),
 300            Object::Quotes => {
 301                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 302            }
 303            Object::BackQuotes => {
 304                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 305            }
 306            Object::AnyQuotes => {
 307                let quote_types = ['\'', '"', '`']; // Types of quotes to handle
 308                let relative_offset = relative_to.to_offset(map, Bias::Left) as isize;
 309
 310                // Find the closest matching quote range
 311                quote_types
 312                    .iter()
 313                    .flat_map(|&quote| {
 314                        // Get ranges for each quote type
 315                        surrounding_markers(
 316                            map,
 317                            relative_to,
 318                            around,
 319                            self.is_multiline(),
 320                            quote,
 321                            quote,
 322                        )
 323                    })
 324                    .min_by_key(|range| calculate_range_distance(range, relative_offset, map))
 325            }
 326            Object::DoubleQuotes => {
 327                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 328            }
 329            Object::VerticalBars => {
 330                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 331            }
 332            Object::Parentheses => {
 333                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 334            }
 335            Object::Tag => {
 336                let head = selection.head();
 337                let range = selection.range();
 338                surrounding_html_tag(map, head, range, around)
 339            }
 340            Object::AnyBrackets => {
 341                let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
 342                let relative_offset = relative_to.to_offset(map, Bias::Left) as isize;
 343
 344                bracket_pairs
 345                    .iter()
 346                    .flat_map(|&(open_bracket, close_bracket)| {
 347                        surrounding_markers(
 348                            map,
 349                            relative_to,
 350                            around,
 351                            self.is_multiline(),
 352                            open_bracket,
 353                            close_bracket,
 354                        )
 355                    })
 356                    .min_by_key(|range| calculate_range_distance(range, relative_offset, map))
 357            }
 358            Object::SquareBrackets => {
 359                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 360            }
 361            Object::CurlyBrackets => {
 362                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 363            }
 364            Object::AngleBrackets => {
 365                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 366            }
 367            Object::Method => text_object(
 368                map,
 369                relative_to,
 370                if around {
 371                    TextObject::AroundFunction
 372                } else {
 373                    TextObject::InsideFunction
 374                },
 375            ),
 376            Object::Comment => text_object(
 377                map,
 378                relative_to,
 379                if around {
 380                    TextObject::AroundComment
 381                } else {
 382                    TextObject::InsideComment
 383                },
 384            ),
 385            Object::Class => text_object(
 386                map,
 387                relative_to,
 388                if around {
 389                    TextObject::AroundClass
 390                } else {
 391                    TextObject::InsideClass
 392                },
 393            ),
 394            Object::Argument => argument(map, relative_to, around),
 395            Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
 396            Object::EntireFile => entire_file(map),
 397        }
 398    }
 399
 400    pub fn expand_selection(
 401        self,
 402        map: &DisplaySnapshot,
 403        selection: &mut Selection<DisplayPoint>,
 404        around: bool,
 405    ) -> bool {
 406        if let Some(range) = self.range(map, selection.clone(), around) {
 407            selection.start = range.start;
 408            selection.end = range.end;
 409            true
 410        } else {
 411            false
 412        }
 413    }
 414}
 415
 416/// Returns a range that surrounds the word `relative_to` is in.
 417///
 418/// If `relative_to` is at the start of a word, return the word.
 419/// If `relative_to` is between words, return the space between.
 420fn in_word(
 421    map: &DisplaySnapshot,
 422    relative_to: DisplayPoint,
 423    ignore_punctuation: bool,
 424) -> Option<Range<DisplayPoint>> {
 425    // Use motion::right so that we consider the character under the cursor when looking for the start
 426    let classifier = map
 427        .buffer_snapshot
 428        .char_classifier_at(relative_to.to_point(map))
 429        .ignore_punctuation(ignore_punctuation);
 430    let start = movement::find_preceding_boundary_display_point(
 431        map,
 432        right(map, relative_to, 1),
 433        movement::FindRange::SingleLine,
 434        |left, right| classifier.kind(left) != classifier.kind(right),
 435    );
 436
 437    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 438        classifier.kind(left) != classifier.kind(right)
 439    });
 440
 441    Some(start..end)
 442}
 443
 444fn in_subword(
 445    map: &DisplaySnapshot,
 446    relative_to: DisplayPoint,
 447    ignore_punctuation: bool,
 448) -> Option<Range<DisplayPoint>> {
 449    let offset = relative_to.to_offset(map, Bias::Left);
 450    // Use motion::right so that we consider the character under the cursor when looking for the start
 451    let classifier = map
 452        .buffer_snapshot
 453        .char_classifier_at(relative_to.to_point(map))
 454        .ignore_punctuation(ignore_punctuation);
 455    let in_subword = map
 456        .buffer_chars_at(offset)
 457        .next()
 458        .map(|(c, _)| {
 459            if classifier.is_word('-') {
 460                !classifier.is_whitespace(c) && c != '_' && c != '-'
 461            } else {
 462                !classifier.is_whitespace(c) && c != '_'
 463            }
 464        })
 465        .unwrap_or(false);
 466
 467    let start = if in_subword {
 468        movement::find_preceding_boundary_display_point(
 469            map,
 470            right(map, relative_to, 1),
 471            movement::FindRange::SingleLine,
 472            |left, right| {
 473                let is_word_start = classifier.kind(left) != classifier.kind(right);
 474                let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
 475                    || left == '_' && right != '_'
 476                    || left.is_lowercase() && right.is_uppercase();
 477                is_word_start || is_subword_start
 478            },
 479        )
 480    } else {
 481        movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 482            let is_word_start = classifier.kind(left) != classifier.kind(right);
 483            let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
 484                || left == '_' && right != '_'
 485                || left.is_lowercase() && right.is_uppercase();
 486            is_word_start || is_subword_start
 487        })
 488    };
 489
 490    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 491        let is_word_end = classifier.kind(left) != classifier.kind(right);
 492        let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
 493            || left != '_' && right == '_'
 494            || left.is_lowercase() && right.is_uppercase();
 495        is_word_end || is_subword_end
 496    });
 497
 498    Some(start..end)
 499}
 500
 501pub fn surrounding_html_tag(
 502    map: &DisplaySnapshot,
 503    head: DisplayPoint,
 504    range: Range<DisplayPoint>,
 505    around: bool,
 506) -> Option<Range<DisplayPoint>> {
 507    fn read_tag(chars: impl Iterator<Item = char>) -> String {
 508        chars
 509            .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
 510            .collect()
 511    }
 512    fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 513        if Some('<') != chars.next() {
 514            return None;
 515        }
 516        Some(read_tag(chars))
 517    }
 518    fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 519        if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
 520            return None;
 521        }
 522        Some(read_tag(chars))
 523    }
 524
 525    let snapshot = &map.buffer_snapshot;
 526    let offset = head.to_offset(map, Bias::Left);
 527    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 528    let buffer = excerpt.buffer();
 529    let offset = excerpt.map_offset_to_buffer(offset);
 530
 531    // Find the most closest to current offset
 532    let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
 533    let mut last_child_node = cursor.node();
 534    while cursor.goto_first_child_for_byte(offset).is_some() {
 535        last_child_node = cursor.node();
 536    }
 537
 538    let mut last_child_node = Some(last_child_node);
 539    while let Some(cur_node) = last_child_node {
 540        if cur_node.child_count() >= 2 {
 541            let first_child = cur_node.child(0);
 542            let last_child = cur_node.child(cur_node.child_count() - 1);
 543            if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
 544                let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
 545                let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
 546                // It needs to be handled differently according to the selection length
 547                let is_valid = if range.end.to_offset(map, Bias::Left)
 548                    - range.start.to_offset(map, Bias::Left)
 549                    <= 1
 550                {
 551                    offset <= last_child.end_byte()
 552                } else {
 553                    range.start.to_offset(map, Bias::Left) >= first_child.start_byte()
 554                        && range.end.to_offset(map, Bias::Left) <= last_child.start_byte() + 1
 555                };
 556                if open_tag.is_some() && open_tag == close_tag && is_valid {
 557                    let range = if around {
 558                        first_child.byte_range().start..last_child.byte_range().end
 559                    } else {
 560                        first_child.byte_range().end..last_child.byte_range().start
 561                    };
 562                    if excerpt.contains_buffer_range(range.clone()) {
 563                        let result = excerpt.map_range_from_buffer(range);
 564                        return Some(
 565                            result.start.to_display_point(map)..result.end.to_display_point(map),
 566                        );
 567                    }
 568                }
 569            }
 570        }
 571        last_child_node = cur_node.parent();
 572    }
 573    None
 574}
 575
 576/// Returns a range that surrounds the word and following whitespace
 577/// relative_to is in.
 578///
 579/// If `relative_to` is at the start of a word, return the word and following whitespace.
 580/// If `relative_to` is between words, return the whitespace back and the following word.
 581///
 582/// if in word
 583///   delete that word
 584///   if there is whitespace following the word, delete that as well
 585///   otherwise, delete any preceding whitespace
 586/// otherwise
 587///   delete whitespace around cursor
 588///   delete word following the cursor
 589fn around_word(
 590    map: &DisplaySnapshot,
 591    relative_to: DisplayPoint,
 592    ignore_punctuation: bool,
 593) -> Option<Range<DisplayPoint>> {
 594    let offset = relative_to.to_offset(map, Bias::Left);
 595    let classifier = map
 596        .buffer_snapshot
 597        .char_classifier_at(offset)
 598        .ignore_punctuation(ignore_punctuation);
 599    let in_word = map
 600        .buffer_chars_at(offset)
 601        .next()
 602        .map(|(c, _)| !classifier.is_whitespace(c))
 603        .unwrap_or(false);
 604
 605    if in_word {
 606        around_containing_word(map, relative_to, ignore_punctuation)
 607    } else {
 608        around_next_word(map, relative_to, ignore_punctuation)
 609    }
 610}
 611
 612/// Calculate distance between a range and a cursor position
 613///
 614/// Returns a score where:
 615/// - Lower values indicate better matches
 616/// - Range containing cursor gets priority (returns range length)
 617/// - For non-containing ranges, uses minimum distance to boundaries as primary factor
 618/// - Range length is used as secondary factor for tiebreaking
 619fn calculate_range_distance(
 620    range: &Range<DisplayPoint>,
 621    cursor_offset: isize,
 622    map: &DisplaySnapshot,
 623) -> isize {
 624    let start_offset = range.start.to_offset(map, Bias::Left) as isize;
 625    let end_offset = range.end.to_offset(map, Bias::Right) as isize;
 626    let range_length = end_offset - start_offset;
 627
 628    // If cursor is inside the range, return range length
 629    if cursor_offset >= start_offset && cursor_offset <= end_offset {
 630        return range_length;
 631    }
 632
 633    // Calculate minimum distance to range boundaries
 634    let start_distance = (cursor_offset - start_offset).abs();
 635    let end_distance = (cursor_offset - end_offset).abs();
 636    let min_distance = start_distance.min(end_distance);
 637
 638    // Use min_distance as primary factor, range_length as secondary
 639    // Multiply by large number to ensure distance is primary factor
 640    min_distance * 10000 + range_length
 641}
 642
 643fn around_subword(
 644    map: &DisplaySnapshot,
 645    relative_to: DisplayPoint,
 646    ignore_punctuation: bool,
 647) -> Option<Range<DisplayPoint>> {
 648    // Use motion::right so that we consider the character under the cursor when looking for the start
 649    let classifier = map
 650        .buffer_snapshot
 651        .char_classifier_at(relative_to.to_point(map))
 652        .ignore_punctuation(ignore_punctuation);
 653    let start = movement::find_preceding_boundary_display_point(
 654        map,
 655        right(map, relative_to, 1),
 656        movement::FindRange::SingleLine,
 657        |left, right| {
 658            let is_word_start = classifier.kind(left) != classifier.kind(right);
 659            let is_subword_start = classifier.is_word('-') && left != '-' && right == '-'
 660                || left != '_' && right == '_'
 661                || left.is_lowercase() && right.is_uppercase();
 662            is_word_start || is_subword_start
 663        },
 664    );
 665
 666    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 667        let is_word_end = classifier.kind(left) != classifier.kind(right);
 668        let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
 669            || left != '_' && right == '_'
 670            || left.is_lowercase() && right.is_uppercase();
 671        is_word_end || is_subword_end
 672    });
 673
 674    Some(start..end)
 675}
 676
 677fn around_containing_word(
 678    map: &DisplaySnapshot,
 679    relative_to: DisplayPoint,
 680    ignore_punctuation: bool,
 681) -> Option<Range<DisplayPoint>> {
 682    in_word(map, relative_to, ignore_punctuation)
 683        .map(|range| expand_to_include_whitespace(map, range, true))
 684}
 685
 686fn around_next_word(
 687    map: &DisplaySnapshot,
 688    relative_to: DisplayPoint,
 689    ignore_punctuation: bool,
 690) -> Option<Range<DisplayPoint>> {
 691    let classifier = map
 692        .buffer_snapshot
 693        .char_classifier_at(relative_to.to_point(map))
 694        .ignore_punctuation(ignore_punctuation);
 695    // Get the start of the word
 696    let start = movement::find_preceding_boundary_display_point(
 697        map,
 698        right(map, relative_to, 1),
 699        FindRange::SingleLine,
 700        |left, right| classifier.kind(left) != classifier.kind(right),
 701    );
 702
 703    let mut word_found = false;
 704    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
 705        let left_kind = classifier.kind(left);
 706        let right_kind = classifier.kind(right);
 707
 708        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
 709
 710        if right_kind != CharKind::Whitespace {
 711            word_found = true;
 712        }
 713
 714        found
 715    });
 716
 717    Some(start..end)
 718}
 719
 720fn entire_file(map: &DisplaySnapshot) -> Option<Range<DisplayPoint>> {
 721    Some(DisplayPoint::zero()..map.max_point())
 722}
 723
 724fn text_object(
 725    map: &DisplaySnapshot,
 726    relative_to: DisplayPoint,
 727    target: TextObject,
 728) -> Option<Range<DisplayPoint>> {
 729    let snapshot = &map.buffer_snapshot;
 730    let offset = relative_to.to_offset(map, Bias::Left);
 731
 732    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 733    let buffer = excerpt.buffer();
 734    let offset = excerpt.map_offset_to_buffer(offset);
 735
 736    let mut matches: Vec<Range<usize>> = buffer
 737        .text_object_ranges(offset..offset, TreeSitterOptions::default())
 738        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
 739        .collect();
 740    matches.sort_by_key(|r| (r.end - r.start));
 741    if let Some(buffer_range) = matches.first() {
 742        let range = excerpt.map_range_from_buffer(buffer_range.clone());
 743        return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
 744    }
 745
 746    let around = target.around()?;
 747    let mut matches: Vec<Range<usize>> = buffer
 748        .text_object_ranges(offset..offset, TreeSitterOptions::default())
 749        .filter_map(|(r, m)| if m == around { Some(r) } else { None })
 750        .collect();
 751    matches.sort_by_key(|r| (r.end - r.start));
 752    let around_range = matches.first()?;
 753
 754    let mut matches: Vec<Range<usize>> = buffer
 755        .text_object_ranges(around_range.clone(), TreeSitterOptions::default())
 756        .filter_map(|(r, m)| if m == target { Some(r) } else { None })
 757        .collect();
 758    matches.sort_by_key(|r| r.start);
 759    if let Some(buffer_range) = matches.first() {
 760        if !buffer_range.is_empty() {
 761            let range = excerpt.map_range_from_buffer(buffer_range.clone());
 762            return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
 763        }
 764    }
 765    let buffer_range = excerpt.map_range_from_buffer(around_range.clone());
 766    return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map));
 767}
 768
 769fn argument(
 770    map: &DisplaySnapshot,
 771    relative_to: DisplayPoint,
 772    around: bool,
 773) -> Option<Range<DisplayPoint>> {
 774    let snapshot = &map.buffer_snapshot;
 775    let offset = relative_to.to_offset(map, Bias::Left);
 776
 777    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
 778    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
 779    let buffer = excerpt.buffer();
 780
 781    fn comma_delimited_range_at(
 782        buffer: &BufferSnapshot,
 783        mut offset: usize,
 784        include_comma: bool,
 785    ) -> Option<Range<usize>> {
 786        // Seek to the first non-whitespace character
 787        offset += buffer
 788            .chars_at(offset)
 789            .take_while(|c| c.is_whitespace())
 790            .map(char::len_utf8)
 791            .sum::<usize>();
 792
 793        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 794            // Filter out empty ranges
 795            if open.end == close.start {
 796                return false;
 797            }
 798
 799            // If the cursor is outside the brackets, ignore them
 800            if open.start == offset || close.end == offset {
 801                return false;
 802            }
 803
 804            // TODO: Is there any better way to filter out string brackets?
 805            // Used to filter out string brackets
 806            matches!(
 807                buffer.chars_at(open.start).next(),
 808                Some('(' | '[' | '{' | '<' | '|')
 809            )
 810        };
 811
 812        // Find the brackets containing the cursor
 813        let (open_bracket, close_bracket) =
 814            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
 815
 816        let inner_bracket_range = open_bracket.end..close_bracket.start;
 817
 818        let layer = buffer.syntax_layer_at(offset)?;
 819        let node = layer.node();
 820        let mut cursor = node.walk();
 821
 822        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
 823        let mut parent_covers_bracket_range = false;
 824        loop {
 825            let node = cursor.node();
 826            let range = node.byte_range();
 827            let covers_bracket_range =
 828                range.start == open_bracket.start && range.end == close_bracket.end;
 829            if parent_covers_bracket_range && !covers_bracket_range {
 830                break;
 831            }
 832            parent_covers_bracket_range = covers_bracket_range;
 833
 834            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
 835            cursor.goto_first_child_for_byte(offset)?;
 836        }
 837
 838        let mut argument_node = cursor.node();
 839
 840        // If the child node is the open bracket, move to the next sibling.
 841        if argument_node.byte_range() == open_bracket {
 842            if !cursor.goto_next_sibling() {
 843                return Some(inner_bracket_range);
 844            }
 845            argument_node = cursor.node();
 846        }
 847        // While the child node is the close bracket or a comma, move to the previous sibling
 848        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
 849            if !cursor.goto_previous_sibling() {
 850                return Some(inner_bracket_range);
 851            }
 852            argument_node = cursor.node();
 853            if argument_node.byte_range() == open_bracket {
 854                return Some(inner_bracket_range);
 855            }
 856        }
 857
 858        // The start and end of the argument range, defaulting to the start and end of the argument node
 859        let mut start = argument_node.start_byte();
 860        let mut end = argument_node.end_byte();
 861
 862        let mut needs_surrounding_comma = include_comma;
 863
 864        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
 865        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
 866        while cursor.goto_previous_sibling() {
 867            let prev = cursor.node();
 868
 869            if prev.start_byte() < open_bracket.end {
 870                start = open_bracket.end;
 871                break;
 872            } else if prev.kind() == "," {
 873                if needs_surrounding_comma {
 874                    start = prev.start_byte();
 875                    needs_surrounding_comma = false;
 876                }
 877                break;
 878            } else if prev.start_byte() < start {
 879                start = prev.start_byte();
 880            }
 881        }
 882
 883        // Do the same for the end of the argument, extending to next comma or the end of the argument list
 884        while cursor.goto_next_sibling() {
 885            let next = cursor.node();
 886
 887            if next.end_byte() > close_bracket.start {
 888                end = close_bracket.start;
 889                break;
 890            } else if next.kind() == "," {
 891                if needs_surrounding_comma {
 892                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
 893                    if let Some(next_arg) = next.next_sibling() {
 894                        end = next_arg.start_byte();
 895                    } else {
 896                        end = next.end_byte();
 897                    }
 898                }
 899                break;
 900            } else if next.end_byte() > end {
 901                end = next.end_byte();
 902            }
 903        }
 904
 905        Some(start..end)
 906    }
 907
 908    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
 909
 910    if excerpt.contains_buffer_range(result.clone()) {
 911        let result = excerpt.map_range_from_buffer(result);
 912        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
 913    } else {
 914        None
 915    }
 916}
 917
 918fn indent(
 919    map: &DisplaySnapshot,
 920    relative_to: DisplayPoint,
 921    around: bool,
 922    include_below: bool,
 923) -> Option<Range<DisplayPoint>> {
 924    let point = relative_to.to_point(map);
 925    let row = point.row;
 926
 927    let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
 928
 929    // Loop backwards until we find a non-blank line with less indent
 930    let mut start_row = row;
 931    for prev_row in (0..row).rev() {
 932        let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
 933        if indent.is_line_empty() {
 934            continue;
 935        }
 936        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
 937            if around {
 938                // When around is true, include the first line with less indent
 939                start_row = prev_row;
 940            }
 941            break;
 942        }
 943        start_row = prev_row;
 944    }
 945
 946    // Loop forwards until we find a non-blank line with less indent
 947    let mut end_row = row;
 948    let max_rows = map.buffer_snapshot.max_row().0;
 949    for next_row in (row + 1)..=max_rows {
 950        let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
 951        if indent.is_line_empty() {
 952            continue;
 953        }
 954        if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
 955            if around && include_below {
 956                // When around is true and including below, include this line
 957                end_row = next_row;
 958            }
 959            break;
 960        }
 961        end_row = next_row;
 962    }
 963
 964    let end_len = map.buffer_snapshot.line_len(MultiBufferRow(end_row));
 965    let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
 966    let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
 967    Some(start..end)
 968}
 969
 970fn sentence(
 971    map: &DisplaySnapshot,
 972    relative_to: DisplayPoint,
 973    around: bool,
 974) -> Option<Range<DisplayPoint>> {
 975    let mut start = None;
 976    let relative_offset = relative_to.to_offset(map, Bias::Left);
 977    let mut previous_end = relative_offset;
 978
 979    let mut chars = map.buffer_chars_at(previous_end).peekable();
 980
 981    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
 982    for (char, offset) in chars
 983        .peek()
 984        .cloned()
 985        .into_iter()
 986        .chain(map.reverse_buffer_chars_at(previous_end))
 987    {
 988        if is_sentence_end(map, offset) {
 989            break;
 990        }
 991
 992        if is_possible_sentence_start(char) {
 993            start = Some(offset);
 994        }
 995
 996        previous_end = offset;
 997    }
 998
 999    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
1000    let mut end = relative_offset;
1001    for (char, offset) in chars {
1002        if start.is_none() && is_possible_sentence_start(char) {
1003            if around {
1004                start = Some(offset);
1005                continue;
1006            } else {
1007                end = offset;
1008                break;
1009            }
1010        }
1011
1012        if char != '\n' {
1013            end = offset + char.len_utf8();
1014        }
1015
1016        if is_sentence_end(map, end) {
1017            break;
1018        }
1019    }
1020
1021    let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
1022    if around {
1023        range = expand_to_include_whitespace(map, range, false);
1024    }
1025
1026    Some(range)
1027}
1028
1029fn is_possible_sentence_start(character: char) -> bool {
1030    !character.is_whitespace() && character != '.'
1031}
1032
1033const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
1034const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
1035const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
1036fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
1037    let mut next_chars = map.buffer_chars_at(offset).peekable();
1038    if let Some((char, _)) = next_chars.next() {
1039        // We are at a double newline. This position is a sentence end.
1040        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
1041            return true;
1042        }
1043
1044        // The next text is not a valid whitespace. This is not a sentence end
1045        if !SENTENCE_END_WHITESPACE.contains(&char) {
1046            return false;
1047        }
1048    }
1049
1050    for (char, _) in map.reverse_buffer_chars_at(offset) {
1051        if SENTENCE_END_PUNCTUATION.contains(&char) {
1052            return true;
1053        }
1054
1055        if !SENTENCE_END_FILLERS.contains(&char) {
1056            return false;
1057        }
1058    }
1059
1060    false
1061}
1062
1063/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
1064/// whitespace to the end first and falls back to the start if there was none.
1065fn expand_to_include_whitespace(
1066    map: &DisplaySnapshot,
1067    range: Range<DisplayPoint>,
1068    stop_at_newline: bool,
1069) -> Range<DisplayPoint> {
1070    let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
1071    let mut whitespace_included = false;
1072
1073    let chars = map.buffer_chars_at(range.end).peekable();
1074    for (char, offset) in chars {
1075        if char == '\n' && stop_at_newline {
1076            break;
1077        }
1078
1079        if char.is_whitespace() {
1080            if char != '\n' {
1081                range.end = offset + char.len_utf8();
1082                whitespace_included = true;
1083            }
1084        } else {
1085            // Found non whitespace. Quit out.
1086            break;
1087        }
1088    }
1089
1090    if !whitespace_included {
1091        for (char, point) in map.reverse_buffer_chars_at(range.start) {
1092            if char == '\n' && stop_at_newline {
1093                break;
1094            }
1095
1096            if !char.is_whitespace() {
1097                break;
1098            }
1099
1100            range.start = point;
1101        }
1102    }
1103
1104    range.start.to_display_point(map)..range.end.to_display_point(map)
1105}
1106
1107/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
1108/// where `relative_to` is in. If `around`, principally returns the range ending
1109/// at the end of the next paragraph.
1110///
1111/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
1112/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
1113/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
1114/// the trailing newline is not subject to subsequent operations).
1115///
1116/// Edge cases:
1117/// - If `around` and if the current paragraph is the last paragraph of the
1118///   file and is blank, then the selection results in an error.
1119/// - If `around` and if the current paragraph is the last paragraph of the
1120///   file and is not blank, then the returned range starts at the start of the
1121///   previous paragraph, if it exists.
1122fn paragraph(
1123    map: &DisplaySnapshot,
1124    relative_to: DisplayPoint,
1125    around: bool,
1126) -> Option<Range<DisplayPoint>> {
1127    let mut paragraph_start = start_of_paragraph(map, relative_to);
1128    let mut paragraph_end = end_of_paragraph(map, relative_to);
1129
1130    let paragraph_end_row = paragraph_end.row();
1131    let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
1132    let point = relative_to.to_point(map);
1133    let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1134
1135    if around {
1136        if paragraph_ends_with_eof {
1137            if current_line_is_empty {
1138                return None;
1139            }
1140
1141            let paragraph_start_row = paragraph_start.row();
1142            if paragraph_start_row.0 != 0 {
1143                let previous_paragraph_last_line_start =
1144                    DisplayPoint::new(paragraph_start_row - 1, 0);
1145                paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
1146            }
1147        } else {
1148            let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
1149            paragraph_end = end_of_paragraph(map, next_paragraph_start);
1150        }
1151    }
1152
1153    let range = paragraph_start..paragraph_end;
1154    Some(range)
1155}
1156
1157/// Returns a position of the start of the current paragraph, where a paragraph
1158/// is defined as a run of non-blank lines or a run of blank lines.
1159pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1160    let point = display_point.to_point(map);
1161    if point.row == 0 {
1162        return DisplayPoint::zero();
1163    }
1164
1165    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1166
1167    for row in (0..point.row).rev() {
1168        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1169        if blank != is_current_line_blank {
1170            return Point::new(row + 1, 0).to_display_point(map);
1171        }
1172    }
1173
1174    DisplayPoint::zero()
1175}
1176
1177/// Returns a position of the end of the current paragraph, where a paragraph
1178/// is defined as a run of non-blank lines or a run of blank lines.
1179/// The trailing newline is excluded from the paragraph.
1180pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1181    let point = display_point.to_point(map);
1182    if point.row == map.buffer_snapshot.max_row().0 {
1183        return map.max_point();
1184    }
1185
1186    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
1187
1188    for row in point.row + 1..map.buffer_snapshot.max_row().0 + 1 {
1189        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
1190        if blank != is_current_line_blank {
1191            let previous_row = row - 1;
1192            return Point::new(
1193                previous_row,
1194                map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
1195            )
1196            .to_display_point(map);
1197        }
1198    }
1199
1200    map.max_point()
1201}
1202
1203fn surrounding_markers(
1204    map: &DisplaySnapshot,
1205    relative_to: DisplayPoint,
1206    around: bool,
1207    search_across_lines: bool,
1208    open_marker: char,
1209    close_marker: char,
1210) -> Option<Range<DisplayPoint>> {
1211    let point = relative_to.to_offset(map, Bias::Left);
1212
1213    let mut matched_closes = 0;
1214    let mut opening = None;
1215
1216    let mut before_ch = match movement::chars_before(map, point).next() {
1217        Some((ch, _)) => ch,
1218        _ => '\0',
1219    };
1220    if let Some((ch, range)) = movement::chars_after(map, point).next() {
1221        if ch == open_marker && before_ch != '\\' {
1222            if open_marker == close_marker {
1223                let mut total = 0;
1224                for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
1225                {
1226                    if ch == '\n' {
1227                        break;
1228                    }
1229                    if ch == open_marker && before_ch != '\\' {
1230                        total += 1;
1231                    }
1232                }
1233                if total % 2 == 0 {
1234                    opening = Some(range)
1235                }
1236            } else {
1237                opening = Some(range)
1238            }
1239        }
1240    }
1241
1242    if opening.is_none() {
1243        let mut chars_before = movement::chars_before(map, point).peekable();
1244        while let Some((ch, range)) = chars_before.next() {
1245            if ch == '\n' && !search_across_lines {
1246                break;
1247            }
1248
1249            if let Some((before_ch, _)) = chars_before.peek() {
1250                if *before_ch == '\\' {
1251                    continue;
1252                }
1253            }
1254
1255            if ch == open_marker {
1256                if matched_closes == 0 {
1257                    opening = Some(range);
1258                    break;
1259                }
1260                matched_closes -= 1;
1261            } else if ch == close_marker {
1262                matched_closes += 1
1263            }
1264        }
1265    }
1266    if opening.is_none() {
1267        for (ch, range) in movement::chars_after(map, point) {
1268            if before_ch != '\\' {
1269                if ch == open_marker {
1270                    opening = Some(range);
1271                    break;
1272                } else if ch == close_marker {
1273                    break;
1274                }
1275            }
1276
1277            before_ch = ch;
1278        }
1279    }
1280
1281    let mut opening = opening?;
1282
1283    let mut matched_opens = 0;
1284    let mut closing = None;
1285    before_ch = match movement::chars_before(map, opening.end).next() {
1286        Some((ch, _)) => ch,
1287        _ => '\0',
1288    };
1289    for (ch, range) in movement::chars_after(map, opening.end) {
1290        if ch == '\n' && !search_across_lines {
1291            break;
1292        }
1293
1294        if before_ch != '\\' {
1295            if ch == close_marker {
1296                if matched_opens == 0 {
1297                    closing = Some(range);
1298                    break;
1299                }
1300                matched_opens -= 1;
1301            } else if ch == open_marker {
1302                matched_opens += 1;
1303            }
1304        }
1305
1306        before_ch = ch;
1307    }
1308
1309    let mut closing = closing?;
1310
1311    if around && !search_across_lines {
1312        let mut found = false;
1313
1314        for (ch, range) in movement::chars_after(map, closing.end) {
1315            if ch.is_whitespace() && ch != '\n' {
1316                found = true;
1317                closing.end = range.end;
1318            } else {
1319                break;
1320            }
1321        }
1322
1323        if !found {
1324            for (ch, range) in movement::chars_before(map, opening.start) {
1325                if ch.is_whitespace() && ch != '\n' {
1326                    opening.start = range.start
1327                } else {
1328                    break;
1329                }
1330            }
1331        }
1332    }
1333
1334    if !around && search_across_lines {
1335        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
1336            if ch == '\n' {
1337                opening.end = range.end
1338            }
1339        }
1340
1341        for (ch, range) in movement::chars_before(map, closing.start) {
1342            if !ch.is_whitespace() {
1343                break;
1344            }
1345            if ch != '\n' {
1346                closing.start = range.start
1347            }
1348        }
1349    }
1350
1351    let result = if around {
1352        opening.start..closing.end
1353    } else {
1354        opening.end..closing.start
1355    };
1356
1357    Some(
1358        map.clip_point(result.start.to_display_point(map), Bias::Left)
1359            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
1360    )
1361}
1362
1363#[cfg(test)]
1364mod test {
1365    use gpui::KeyBinding;
1366    use indoc::indoc;
1367
1368    use crate::{
1369        object::AnyBrackets,
1370        state::Mode,
1371        test::{NeovimBackedTestContext, VimTestContext},
1372    };
1373
1374    const WORD_LOCATIONS: &str = indoc! {"
1375        The quick ˇbrowˇnˇ•••
1376        fox ˇjuˇmpsˇ over
1377        the lazy dogˇ••
1378        ˇ
1379        ˇ
1380        ˇ
1381        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
1382        ˇ••
1383        ˇ••
1384        ˇ  fox-jumpˇs over
1385        the lazy dogˇ•
1386        ˇ
1387        "
1388    };
1389
1390    #[gpui::test]
1391    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
1392        let mut cx = NeovimBackedTestContext::new(cx).await;
1393
1394        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
1395            .await
1396            .assert_matches();
1397        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
1398            .await
1399            .assert_matches();
1400        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1401            .await
1402            .assert_matches();
1403        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1404            .await
1405            .assert_matches();
1406    }
1407
1408    #[gpui::test]
1409    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1410        let mut cx = NeovimBackedTestContext::new(cx).await;
1411
1412        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1413            .await
1414            .assert_matches();
1415        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1416            .await
1417            .assert_matches();
1418        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1419            .await
1420            .assert_matches();
1421        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1422            .await
1423            .assert_matches();
1424    }
1425
1426    #[gpui::test]
1427    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1428        let mut cx = NeovimBackedTestContext::new(cx).await;
1429
1430        /*
1431                cx.set_shared_state("The quick ˇbrown\nfox").await;
1432                cx.simulate_shared_keystrokes(["v"]).await;
1433                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1434                cx.simulate_shared_keystrokes(["i", "w"]).await;
1435                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1436        */
1437        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1438        cx.simulate_shared_keystrokes("v").await;
1439        cx.shared_state()
1440            .await
1441            .assert_eq("The quick brown\n«\nˇ»fox");
1442        cx.simulate_shared_keystrokes("i w").await;
1443        cx.shared_state()
1444            .await
1445            .assert_eq("The quick brown\n«\nˇ»fox");
1446
1447        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1448            .await
1449            .assert_matches();
1450        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1451            .await
1452            .assert_matches();
1453    }
1454
1455    const PARAGRAPH_EXAMPLES: &[&str] = &[
1456        // Single line
1457        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1458        // Multiple lines without empty lines
1459        indoc! {"
1460            ˇThe quick brownˇ
1461            ˇfox jumps overˇ
1462            the lazy dog.ˇ
1463        "},
1464        // Heading blank paragraph and trailing normal paragraph
1465        indoc! {"
1466            ˇ
1467            ˇ
1468            ˇThe quick brown fox jumps
1469            ˇover the lazy dog.
1470            ˇ
1471            ˇ
1472            ˇThe quick brown fox jumpsˇ
1473            ˇover the lazy dog.ˇ
1474        "},
1475        // Inserted blank paragraph and trailing blank paragraph
1476        indoc! {"
1477            ˇThe quick brown fox jumps
1478            ˇover the lazy dog.
1479            ˇ
1480            ˇ
1481            ˇ
1482            ˇThe quick brown fox jumpsˇ
1483            ˇover the lazy dog.ˇ
1484            ˇ
1485            ˇ
1486            ˇ
1487        "},
1488        // "Blank" paragraph with whitespace characters
1489        indoc! {"
1490            ˇThe quick brown fox jumps
1491            over the lazy dog.
1492
1493            ˇ \t
1494
1495            ˇThe quick brown fox jumps
1496            over the lazy dog.ˇ
1497            ˇ
1498            ˇ \t
1499            \t \t
1500        "},
1501        // Single line "paragraphs", where selection size might be zero.
1502        indoc! {"
1503            ˇThe quick brown fox jumps over the lazy dog.
1504            ˇ
1505            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1506            ˇ
1507        "},
1508    ];
1509
1510    #[gpui::test]
1511    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1512        let mut cx = NeovimBackedTestContext::new(cx).await;
1513
1514        for paragraph_example in PARAGRAPH_EXAMPLES {
1515            cx.simulate_at_each_offset("c i p", paragraph_example)
1516                .await
1517                .assert_matches();
1518            cx.simulate_at_each_offset("c a p", paragraph_example)
1519                .await
1520                .assert_matches();
1521        }
1522    }
1523
1524    #[gpui::test]
1525    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1526        let mut cx = NeovimBackedTestContext::new(cx).await;
1527
1528        for paragraph_example in PARAGRAPH_EXAMPLES {
1529            cx.simulate_at_each_offset("d i p", paragraph_example)
1530                .await
1531                .assert_matches();
1532            cx.simulate_at_each_offset("d a p", paragraph_example)
1533                .await
1534                .assert_matches();
1535        }
1536    }
1537
1538    #[gpui::test]
1539    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1540        let mut cx = NeovimBackedTestContext::new(cx).await;
1541
1542        const EXAMPLES: &[&str] = &[
1543            indoc! {"
1544                ˇThe quick brown
1545                fox jumps over
1546                the lazy dog.
1547            "},
1548            indoc! {"
1549                ˇ
1550
1551                ˇThe quick brown fox jumps
1552                over the lazy dog.
1553                ˇ
1554
1555                ˇThe quick brown fox jumps
1556                over the lazy dog.
1557            "},
1558            indoc! {"
1559                ˇThe quick brown fox jumps over the lazy dog.
1560                ˇ
1561                ˇThe quick brown fox jumps over the lazy dog.
1562
1563            "},
1564        ];
1565
1566        for paragraph_example in EXAMPLES {
1567            cx.simulate_at_each_offset("v i p", paragraph_example)
1568                .await
1569                .assert_matches();
1570            cx.simulate_at_each_offset("v a p", paragraph_example)
1571                .await
1572                .assert_matches();
1573        }
1574    }
1575
1576    // Test string with "`" for opening surrounders and "'" for closing surrounders
1577    const SURROUNDING_MARKER_STRING: &str = indoc! {"
1578        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1579        'ˇfox juˇmps ov`ˇer
1580        the ˇlazy d'o`ˇg"};
1581
1582    const SURROUNDING_OBJECTS: &[(char, char)] = &[
1583        ('"', '"'), // Double Quote
1584        ('(', ')'), // Parentheses
1585    ];
1586
1587    #[gpui::test]
1588    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1589        let mut cx = NeovimBackedTestContext::new(cx).await;
1590
1591        for (start, end) in SURROUNDING_OBJECTS {
1592            let marked_string = SURROUNDING_MARKER_STRING
1593                .replace('`', &start.to_string())
1594                .replace('\'', &end.to_string());
1595
1596            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1597                .await
1598                .assert_matches();
1599            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1600                .await
1601                .assert_matches();
1602            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1603                .await
1604                .assert_matches();
1605            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1606                .await
1607                .assert_matches();
1608        }
1609    }
1610    #[gpui::test]
1611    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1612        let mut cx = NeovimBackedTestContext::new(cx).await;
1613        cx.set_shared_wrap(12).await;
1614
1615        cx.set_shared_state(indoc! {
1616            "\"ˇhello world\"!"
1617        })
1618        .await;
1619        cx.simulate_shared_keystrokes("v i \"").await;
1620        cx.shared_state().await.assert_eq(indoc! {
1621            "\"«hello worldˇ»\"!"
1622        });
1623
1624        cx.set_shared_state(indoc! {
1625            "\"hˇello world\"!"
1626        })
1627        .await;
1628        cx.simulate_shared_keystrokes("v i \"").await;
1629        cx.shared_state().await.assert_eq(indoc! {
1630            "\"«hello worldˇ»\"!"
1631        });
1632
1633        cx.set_shared_state(indoc! {
1634            "helˇlo \"world\"!"
1635        })
1636        .await;
1637        cx.simulate_shared_keystrokes("v i \"").await;
1638        cx.shared_state().await.assert_eq(indoc! {
1639            "hello \"«worldˇ»\"!"
1640        });
1641
1642        cx.set_shared_state(indoc! {
1643            "hello \"wˇorld\"!"
1644        })
1645        .await;
1646        cx.simulate_shared_keystrokes("v i \"").await;
1647        cx.shared_state().await.assert_eq(indoc! {
1648            "hello \"«worldˇ»\"!"
1649        });
1650
1651        cx.set_shared_state(indoc! {
1652            "hello \"wˇorld\"!"
1653        })
1654        .await;
1655        cx.simulate_shared_keystrokes("v a \"").await;
1656        cx.shared_state().await.assert_eq(indoc! {
1657            "hello« \"world\"ˇ»!"
1658        });
1659
1660        cx.set_shared_state(indoc! {
1661            "hello \"wˇorld\" !"
1662        })
1663        .await;
1664        cx.simulate_shared_keystrokes("v a \"").await;
1665        cx.shared_state().await.assert_eq(indoc! {
1666            "hello «\"world\" ˇ»!"
1667        });
1668
1669        cx.set_shared_state(indoc! {
1670            "hello \"wˇorld\"1671            goodbye"
1672        })
1673        .await;
1674        cx.simulate_shared_keystrokes("v a \"").await;
1675        cx.shared_state().await.assert_eq(indoc! {
1676            "hello «\"world\" ˇ»
1677            goodbye"
1678        });
1679    }
1680
1681    #[gpui::test]
1682    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1683        let mut cx = NeovimBackedTestContext::new(cx).await;
1684
1685        cx.set_shared_state(indoc! {
1686            "func empty(a string) bool {
1687               if a == \"\" {
1688                  return true
1689               }
1690               ˇreturn false
1691            }"
1692        })
1693        .await;
1694        cx.simulate_shared_keystrokes("v i {").await;
1695        cx.shared_state().await.assert_eq(indoc! {"
1696            func empty(a string) bool {
1697            «   if a == \"\" {
1698                  return true
1699               }
1700               return false
1701            ˇ»}"});
1702        cx.set_shared_state(indoc! {
1703            "func empty(a string) bool {
1704                 if a == \"\" {
1705                     ˇreturn true
1706                 }
1707                 return false
1708            }"
1709        })
1710        .await;
1711        cx.simulate_shared_keystrokes("v i {").await;
1712        cx.shared_state().await.assert_eq(indoc! {"
1713            func empty(a string) bool {
1714                 if a == \"\" {
1715            «         return true
1716            ˇ»     }
1717                 return false
1718            }"});
1719
1720        cx.set_shared_state(indoc! {
1721            "func empty(a string) bool {
1722                 if a == \"\" ˇ{
1723                     return true
1724                 }
1725                 return false
1726            }"
1727        })
1728        .await;
1729        cx.simulate_shared_keystrokes("v i {").await;
1730        cx.shared_state().await.assert_eq(indoc! {"
1731            func empty(a string) bool {
1732                 if a == \"\" {
1733            «         return true
1734            ˇ»     }
1735                 return false
1736            }"});
1737    }
1738
1739    #[gpui::test]
1740    async fn test_singleline_surrounding_character_objects_with_escape(
1741        cx: &mut gpui::TestAppContext,
1742    ) {
1743        let mut cx = NeovimBackedTestContext::new(cx).await;
1744        cx.set_shared_state(indoc! {
1745            "h\"e\\\"lˇlo \\\"world\"!"
1746        })
1747        .await;
1748        cx.simulate_shared_keystrokes("v i \"").await;
1749        cx.shared_state().await.assert_eq(indoc! {
1750            "h\"«e\\\"llo \\\"worldˇ»\"!"
1751        });
1752
1753        cx.set_shared_state(indoc! {
1754            "hello \"teˇst \\\"inside\\\" world\""
1755        })
1756        .await;
1757        cx.simulate_shared_keystrokes("v i \"").await;
1758        cx.shared_state().await.assert_eq(indoc! {
1759            "hello \"«test \\\"inside\\\" worldˇ»\""
1760        });
1761    }
1762
1763    #[gpui::test]
1764    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1765        let mut cx = VimTestContext::new(cx, true).await;
1766        cx.set_state(
1767            indoc! {"
1768            fn boop() {
1769                baz(ˇ|a, b| { bar(|j, k| { })})
1770            }"
1771            },
1772            Mode::Normal,
1773        );
1774        cx.simulate_keystrokes("c i |");
1775        cx.assert_state(
1776            indoc! {"
1777            fn boop() {
1778                baz(|ˇ| { bar(|j, k| { })})
1779            }"
1780            },
1781            Mode::Insert,
1782        );
1783        cx.simulate_keystrokes("escape 1 8 |");
1784        cx.assert_state(
1785            indoc! {"
1786            fn boop() {
1787                baz(|| { bar(ˇ|j, k| { })})
1788            }"
1789            },
1790            Mode::Normal,
1791        );
1792
1793        cx.simulate_keystrokes("v a |");
1794        cx.assert_state(
1795            indoc! {"
1796            fn boop() {
1797                baz(|| { bar(«|j, k| ˇ»{ })})
1798            }"
1799            },
1800            Mode::Visual,
1801        );
1802    }
1803
1804    #[gpui::test]
1805    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1806        let mut cx = VimTestContext::new(cx, true).await;
1807
1808        // Generic arguments
1809        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1810        cx.simulate_keystrokes("v i a");
1811        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1812
1813        // Function arguments
1814        cx.set_state(
1815            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1816            Mode::Normal,
1817        );
1818        cx.simulate_keystrokes("d a a");
1819        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1820
1821        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1822        cx.simulate_keystrokes("v a a");
1823        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1824
1825        // Tuple, vec, and array arguments
1826        cx.set_state(
1827            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1828            Mode::Normal,
1829        );
1830        cx.simulate_keystrokes("c i a");
1831        cx.assert_state(
1832            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1833            Mode::Insert,
1834        );
1835
1836        cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1837        cx.simulate_keystrokes("c a a");
1838        cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1839
1840        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1841        cx.simulate_keystrokes("c i a");
1842        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1843
1844        cx.set_state(
1845            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1846            Mode::Normal,
1847        );
1848        cx.simulate_keystrokes("c a a");
1849        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1850
1851        // Cursor immediately before / after brackets
1852        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1853        cx.simulate_keystrokes("v i a");
1854        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1855
1856        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1857        cx.simulate_keystrokes("v i a");
1858        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1859    }
1860
1861    #[gpui::test]
1862    async fn test_indent_object(cx: &mut gpui::TestAppContext) {
1863        let mut cx = VimTestContext::new(cx, true).await;
1864
1865        // Base use case
1866        cx.set_state(
1867            indoc! {"
1868                fn boop() {
1869                    // Comment
1870                    baz();ˇ
1871
1872                    loop {
1873                        bar(1);
1874                        bar(2);
1875                    }
1876
1877                    result
1878                }
1879            "},
1880            Mode::Normal,
1881        );
1882        cx.simulate_keystrokes("v i i");
1883        cx.assert_state(
1884            indoc! {"
1885                fn boop() {
1886                «    // Comment
1887                    baz();
1888
1889                    loop {
1890                        bar(1);
1891                        bar(2);
1892                    }
1893
1894                    resultˇ»
1895                }
1896            "},
1897            Mode::Visual,
1898        );
1899
1900        // Around indent (include line above)
1901        cx.set_state(
1902            indoc! {"
1903                const ABOVE: str = true;
1904                fn boop() {
1905
1906                    hello();
1907                    worˇld()
1908                }
1909            "},
1910            Mode::Normal,
1911        );
1912        cx.simulate_keystrokes("v a i");
1913        cx.assert_state(
1914            indoc! {"
1915                const ABOVE: str = true;
1916                «fn boop() {
1917
1918                    hello();
1919                    world()ˇ»
1920                }
1921            "},
1922            Mode::Visual,
1923        );
1924
1925        // Around indent (include line above & below)
1926        cx.set_state(
1927            indoc! {"
1928                const ABOVE: str = true;
1929                fn boop() {
1930                    hellˇo();
1931                    world()
1932
1933                }
1934                const BELOW: str = true;
1935            "},
1936            Mode::Normal,
1937        );
1938        cx.simulate_keystrokes("c a shift-i");
1939        cx.assert_state(
1940            indoc! {"
1941                const ABOVE: str = true;
1942                ˇ
1943                const BELOW: str = true;
1944            "},
1945            Mode::Insert,
1946        );
1947    }
1948
1949    #[gpui::test]
1950    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1951        let mut cx = NeovimBackedTestContext::new(cx).await;
1952
1953        for (start, end) in SURROUNDING_OBJECTS {
1954            let marked_string = SURROUNDING_MARKER_STRING
1955                .replace('`', &start.to_string())
1956                .replace('\'', &end.to_string());
1957
1958            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
1959                .await
1960                .assert_matches();
1961            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
1962                .await
1963                .assert_matches();
1964            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
1965                .await
1966                .assert_matches();
1967            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
1968                .await
1969                .assert_matches();
1970        }
1971    }
1972
1973    #[gpui::test]
1974    async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
1975        let mut cx = VimTestContext::new(cx, true).await;
1976
1977        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
1978            // Single quotes
1979            (
1980                "c i q",
1981                "Thisˇ is a 'quote' example.",
1982                "This is a 'ˇ' example.",
1983                Mode::Insert,
1984            ),
1985            (
1986                "c a q",
1987                "Thisˇ is a 'quote' example.",
1988                "This is a ˇexample.",
1989                Mode::Insert,
1990            ),
1991            (
1992                "c i q",
1993                "This is a \"simple 'qˇuote'\" example.",
1994                "This is a \"simple 'ˇ'\" example.",
1995                Mode::Insert,
1996            ),
1997            (
1998                "c a q",
1999                "This is a \"simple 'qˇuote'\" example.",
2000                "This is a \"simpleˇ\" example.",
2001                Mode::Insert,
2002            ),
2003            (
2004                "c i q",
2005                "This is a 'qˇuote' example.",
2006                "This is a 'ˇ' example.",
2007                Mode::Insert,
2008            ),
2009            (
2010                "c a q",
2011                "This is a 'qˇuote' example.",
2012                "This is a ˇexample.",
2013                Mode::Insert,
2014            ),
2015            (
2016                "d i q",
2017                "This is a 'qˇuote' example.",
2018                "This is a 'ˇ' example.",
2019                Mode::Normal,
2020            ),
2021            (
2022                "d a q",
2023                "This is a 'qˇuote' example.",
2024                "This is a ˇexample.",
2025                Mode::Normal,
2026            ),
2027            // Double quotes
2028            (
2029                "c i q",
2030                "This is a \"qˇuote\" example.",
2031                "This is a \"ˇ\" example.",
2032                Mode::Insert,
2033            ),
2034            (
2035                "c a q",
2036                "This is a \"qˇuote\" example.",
2037                "This is a ˇexample.",
2038                Mode::Insert,
2039            ),
2040            (
2041                "d i q",
2042                "This is a \"qˇuote\" example.",
2043                "This is a \"ˇ\" example.",
2044                Mode::Normal,
2045            ),
2046            (
2047                "d a q",
2048                "This is a \"qˇuote\" example.",
2049                "This is a ˇexample.",
2050                Mode::Normal,
2051            ),
2052            // Back quotes
2053            (
2054                "c i q",
2055                "This is a `qˇuote` example.",
2056                "This is a `ˇ` example.",
2057                Mode::Insert,
2058            ),
2059            (
2060                "c a q",
2061                "This is a `qˇuote` example.",
2062                "This is a ˇexample.",
2063                Mode::Insert,
2064            ),
2065            (
2066                "d i q",
2067                "This is a `qˇuote` example.",
2068                "This is a `ˇ` example.",
2069                Mode::Normal,
2070            ),
2071            (
2072                "d a q",
2073                "This is a `qˇuote` example.",
2074                "This is a ˇexample.",
2075                Mode::Normal,
2076            ),
2077        ];
2078
2079        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2080            cx.set_state(initial_state, Mode::Normal);
2081
2082            cx.simulate_keystrokes(keystrokes);
2083
2084            cx.assert_state(expected_state, *expected_mode);
2085        }
2086
2087        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2088            ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2089            ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2090            ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2091            ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
2092            ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2093            ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2094            ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
2095            ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
2096            ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2097            ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2098            ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2099            ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
2100        ];
2101
2102        for (keystrokes, initial_state, mode) in INVALID_CASES {
2103            cx.set_state(initial_state, Mode::Normal);
2104
2105            cx.simulate_keystrokes(keystrokes);
2106
2107            cx.assert_state(initial_state, *mode);
2108        }
2109    }
2110
2111    #[gpui::test]
2112    async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
2113        let mut cx = VimTestContext::new(cx, true).await;
2114        cx.update(|_, cx| {
2115            cx.bind_keys([KeyBinding::new(
2116                "b",
2117                AnyBrackets,
2118                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
2119            )]);
2120        });
2121
2122        const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
2123            // Bracket (Parentheses)
2124            (
2125                "c i b",
2126                "Thisˇ is a (simple [quote]) example.",
2127                "This is a (ˇ) example.",
2128                Mode::Insert,
2129            ),
2130            (
2131                "c i b",
2132                "This is a [simple (qˇuote)] example.",
2133                "This is a [simple (ˇ)] example.",
2134                Mode::Insert,
2135            ),
2136            (
2137                "c a b",
2138                "This is a [simple (qˇuote)] example.",
2139                "This is a [simple ˇ] example.",
2140                Mode::Insert,
2141            ),
2142            (
2143                "c a b",
2144                "Thisˇ is a (simple [quote]) example.",
2145                "This is a ˇ example.",
2146                Mode::Insert,
2147            ),
2148            (
2149                "c i b",
2150                "This is a (qˇuote) example.",
2151                "This is a (ˇ) example.",
2152                Mode::Insert,
2153            ),
2154            (
2155                "c a b",
2156                "This is a (qˇuote) example.",
2157                "This is a ˇ example.",
2158                Mode::Insert,
2159            ),
2160            (
2161                "d i b",
2162                "This is a (qˇuote) example.",
2163                "This is a (ˇ) example.",
2164                Mode::Normal,
2165            ),
2166            (
2167                "d a b",
2168                "This is a (qˇuote) example.",
2169                "This is a ˇ example.",
2170                Mode::Normal,
2171            ),
2172            // Square brackets
2173            (
2174                "c i b",
2175                "This is a [qˇuote] example.",
2176                "This is a [ˇ] example.",
2177                Mode::Insert,
2178            ),
2179            (
2180                "c a b",
2181                "This is a [qˇuote] example.",
2182                "This is a ˇ example.",
2183                Mode::Insert,
2184            ),
2185            (
2186                "d i b",
2187                "This is a [qˇuote] example.",
2188                "This is a [ˇ] example.",
2189                Mode::Normal,
2190            ),
2191            (
2192                "d a b",
2193                "This is a [qˇuote] example.",
2194                "This is a ˇ example.",
2195                Mode::Normal,
2196            ),
2197            // Curly brackets
2198            (
2199                "c i b",
2200                "This is a {qˇuote} example.",
2201                "This is a {ˇ} example.",
2202                Mode::Insert,
2203            ),
2204            (
2205                "c a b",
2206                "This is a {qˇuote} example.",
2207                "This is a ˇ example.",
2208                Mode::Insert,
2209            ),
2210            (
2211                "d i b",
2212                "This is a {qˇuote} example.",
2213                "This is a {ˇ} example.",
2214                Mode::Normal,
2215            ),
2216            (
2217                "d a b",
2218                "This is a {qˇuote} example.",
2219                "This is a ˇ example.",
2220                Mode::Normal,
2221            ),
2222        ];
2223
2224        for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
2225            cx.set_state(initial_state, Mode::Normal);
2226
2227            cx.simulate_keystrokes(keystrokes);
2228
2229            cx.assert_state(expected_state, *expected_mode);
2230        }
2231
2232        const INVALID_CASES: &[(&str, &str, Mode)] = &[
2233            ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2234            ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2235            ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2236            ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
2237            ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2238            ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2239            ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2240            ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
2241            ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2242            ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2243            ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2244            ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
2245        ];
2246
2247        for (keystrokes, initial_state, mode) in INVALID_CASES {
2248            cx.set_state(initial_state, Mode::Normal);
2249
2250            cx.simulate_keystrokes(keystrokes);
2251
2252            cx.assert_state(initial_state, *mode);
2253        }
2254    }
2255
2256    #[gpui::test]
2257    async fn test_tags(cx: &mut gpui::TestAppContext) {
2258        let mut cx = VimTestContext::new_html(cx).await;
2259
2260        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
2261        cx.simulate_keystrokes("v i t");
2262        cx.assert_state(
2263            "<html><head></head><body><b>«hi!ˇ»</b></body>",
2264            Mode::Visual,
2265        );
2266        cx.simulate_keystrokes("a t");
2267        cx.assert_state(
2268            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
2269            Mode::Visual,
2270        );
2271        cx.simulate_keystrokes("a t");
2272        cx.assert_state(
2273            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
2274            Mode::Visual,
2275        );
2276
2277        // The cursor is before the tag
2278        cx.set_state(
2279            "<html><head></head><body> ˇ  <b>hi!</b></body>",
2280            Mode::Normal,
2281        );
2282        cx.simulate_keystrokes("v i t");
2283        cx.assert_state(
2284            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
2285            Mode::Visual,
2286        );
2287        cx.simulate_keystrokes("a t");
2288        cx.assert_state(
2289            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
2290            Mode::Visual,
2291        );
2292
2293        // The cursor is in the open tag
2294        cx.set_state(
2295            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
2296            Mode::Normal,
2297        );
2298        cx.simulate_keystrokes("v a t");
2299        cx.assert_state(
2300            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
2301            Mode::Visual,
2302        );
2303        cx.simulate_keystrokes("i t");
2304        cx.assert_state(
2305            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
2306            Mode::Visual,
2307        );
2308
2309        // current selection length greater than 1
2310        cx.set_state(
2311            "<html><head></head><body><«b>hi!ˇ»</b></body>",
2312            Mode::Visual,
2313        );
2314        cx.simulate_keystrokes("i t");
2315        cx.assert_state(
2316            "<html><head></head><body><b>«hi!ˇ»</b></body>",
2317            Mode::Visual,
2318        );
2319        cx.simulate_keystrokes("a t");
2320        cx.assert_state(
2321            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
2322            Mode::Visual,
2323        );
2324
2325        cx.set_state(
2326            "<html><head></head><body><«b>hi!</ˇ»b></body>",
2327            Mode::Visual,
2328        );
2329        cx.simulate_keystrokes("a t");
2330        cx.assert_state(
2331            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
2332            Mode::Visual,
2333        );
2334    }
2335}