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