object.rs

   1use std::ops::Range;
   2
   3use crate::{motion::right, state::Mode, Vim};
   4use editor::{
   5    display_map::{DisplaySnapshot, ToDisplayPoint},
   6    movement::{self, FindRange},
   7    Bias, DisplayPoint, Editor,
   8};
   9
  10use itertools::Itertools;
  11
  12use gpui::{actions, impl_actions, ViewContext};
  13use language::{BufferSnapshot, CharKind, Point, Selection};
  14use multi_buffer::MultiBufferRow;
  15use serde::Deserialize;
  16
  17#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
  18pub enum Object {
  19    Word { ignore_punctuation: bool },
  20    Sentence,
  21    Paragraph,
  22    Quotes,
  23    BackQuotes,
  24    DoubleQuotes,
  25    VerticalBars,
  26    Parentheses,
  27    SquareBrackets,
  28    CurlyBrackets,
  29    AngleBrackets,
  30    Argument,
  31    Tag,
  32}
  33
  34#[derive(Clone, Deserialize, PartialEq)]
  35#[serde(rename_all = "camelCase")]
  36struct Word {
  37    #[serde(default)]
  38    ignore_punctuation: bool,
  39}
  40
  41impl_actions!(vim, [Word]);
  42
  43actions!(
  44    vim,
  45    [
  46        Sentence,
  47        Paragraph,
  48        Quotes,
  49        BackQuotes,
  50        DoubleQuotes,
  51        VerticalBars,
  52        Parentheses,
  53        SquareBrackets,
  54        CurlyBrackets,
  55        AngleBrackets,
  56        Argument,
  57        Tag
  58    ]
  59);
  60
  61pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
  62    Vim::action(
  63        editor,
  64        cx,
  65        |vim, &Word { ignore_punctuation }: &Word, cx| {
  66            vim.object(Object::Word { ignore_punctuation }, cx)
  67        },
  68    );
  69    Vim::action(editor, cx, |vim, _: &Tag, cx| vim.object(Object::Tag, cx));
  70    Vim::action(editor, cx, |vim, _: &Sentence, cx| {
  71        vim.object(Object::Sentence, cx)
  72    });
  73    Vim::action(editor, cx, |vim, _: &Paragraph, cx| {
  74        vim.object(Object::Paragraph, cx)
  75    });
  76    Vim::action(editor, cx, |vim, _: &Quotes, cx| {
  77        vim.object(Object::Quotes, cx)
  78    });
  79    Vim::action(editor, cx, |vim, _: &BackQuotes, cx| {
  80        vim.object(Object::BackQuotes, cx)
  81    });
  82    Vim::action(editor, cx, |vim, _: &DoubleQuotes, cx| {
  83        vim.object(Object::DoubleQuotes, cx)
  84    });
  85    Vim::action(editor, cx, |vim, _: &Parentheses, cx| {
  86        vim.object(Object::Parentheses, cx)
  87    });
  88    Vim::action(editor, cx, |vim, _: &SquareBrackets, cx| {
  89        vim.object(Object::SquareBrackets, cx)
  90    });
  91    Vim::action(editor, cx, |vim, _: &CurlyBrackets, cx| {
  92        vim.object(Object::CurlyBrackets, cx)
  93    });
  94    Vim::action(editor, cx, |vim, _: &AngleBrackets, cx| {
  95        vim.object(Object::AngleBrackets, cx)
  96    });
  97    Vim::action(editor, cx, |vim, _: &VerticalBars, cx| {
  98        vim.object(Object::VerticalBars, cx)
  99    });
 100    Vim::action(editor, cx, |vim, _: &Argument, cx| {
 101        vim.object(Object::Argument, cx)
 102    });
 103}
 104
 105impl Vim {
 106    fn object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
 107        match self.mode {
 108            Mode::Normal => self.normal_object(object, cx),
 109            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => self.visual_object(object, cx),
 110            Mode::Insert | Mode::Replace => {
 111                // Shouldn't execute a text object in insert mode. Ignoring
 112            }
 113        }
 114    }
 115}
 116
 117impl Object {
 118    pub fn is_multiline(self) -> bool {
 119        match self {
 120            Object::Word { .. }
 121            | Object::Quotes
 122            | Object::BackQuotes
 123            | Object::VerticalBars
 124            | Object::DoubleQuotes => false,
 125            Object::Sentence
 126            | Object::Paragraph
 127            | Object::Parentheses
 128            | Object::Tag
 129            | Object::AngleBrackets
 130            | Object::CurlyBrackets
 131            | Object::SquareBrackets
 132            | Object::Argument => true,
 133        }
 134    }
 135
 136    pub fn always_expands_both_ways(self) -> bool {
 137        match self {
 138            Object::Word { .. } | Object::Sentence | Object::Paragraph | Object::Argument => false,
 139            Object::Quotes
 140            | Object::BackQuotes
 141            | Object::DoubleQuotes
 142            | Object::VerticalBars
 143            | Object::Parentheses
 144            | Object::SquareBrackets
 145            | Object::Tag
 146            | Object::CurlyBrackets
 147            | Object::AngleBrackets => true,
 148        }
 149    }
 150
 151    pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
 152        match self {
 153            Object::Word { .. }
 154            | Object::Sentence
 155            | Object::Quotes
 156            | Object::BackQuotes
 157            | Object::DoubleQuotes => {
 158                if current_mode == Mode::VisualBlock {
 159                    Mode::VisualBlock
 160                } else {
 161                    Mode::Visual
 162                }
 163            }
 164            Object::Parentheses
 165            | Object::SquareBrackets
 166            | Object::CurlyBrackets
 167            | Object::AngleBrackets
 168            | Object::VerticalBars
 169            | Object::Tag
 170            | Object::Argument => Mode::Visual,
 171            Object::Paragraph => Mode::VisualLine,
 172        }
 173    }
 174
 175    pub fn range(
 176        self,
 177        map: &DisplaySnapshot,
 178        selection: Selection<DisplayPoint>,
 179        around: bool,
 180    ) -> Option<Range<DisplayPoint>> {
 181        let relative_to = selection.head();
 182        match self {
 183            Object::Word { ignore_punctuation } => {
 184                if around {
 185                    around_word(map, relative_to, ignore_punctuation)
 186                } else {
 187                    in_word(map, relative_to, ignore_punctuation)
 188                }
 189            }
 190            Object::Sentence => sentence(map, relative_to, around),
 191            Object::Paragraph => paragraph(map, relative_to, around),
 192            Object::Quotes => {
 193                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
 194            }
 195            Object::BackQuotes => {
 196                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
 197            }
 198            Object::DoubleQuotes => {
 199                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
 200            }
 201            Object::VerticalBars => {
 202                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
 203            }
 204            Object::Parentheses => {
 205                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
 206            }
 207            Object::Tag => {
 208                let head = selection.head();
 209                let range = selection.range();
 210                surrounding_html_tag(map, head, range, around)
 211            }
 212            Object::SquareBrackets => {
 213                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
 214            }
 215            Object::CurlyBrackets => {
 216                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
 217            }
 218            Object::AngleBrackets => {
 219                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
 220            }
 221            Object::Argument => argument(map, relative_to, around),
 222        }
 223    }
 224
 225    pub fn expand_selection(
 226        self,
 227        map: &DisplaySnapshot,
 228        selection: &mut Selection<DisplayPoint>,
 229        around: bool,
 230    ) -> bool {
 231        if let Some(range) = self.range(map, selection.clone(), around) {
 232            selection.start = range.start;
 233            selection.end = range.end;
 234            true
 235        } else {
 236            false
 237        }
 238    }
 239}
 240
 241/// Returns a range that surrounds the word `relative_to` is in.
 242///
 243/// If `relative_to` is at the start of a word, return the word.
 244/// If `relative_to` is between words, return the space between.
 245fn in_word(
 246    map: &DisplaySnapshot,
 247    relative_to: DisplayPoint,
 248    ignore_punctuation: bool,
 249) -> Option<Range<DisplayPoint>> {
 250    // Use motion::right so that we consider the character under the cursor when looking for the start
 251    let classifier = map
 252        .buffer_snapshot
 253        .char_classifier_at(relative_to.to_point(map))
 254        .ignore_punctuation(ignore_punctuation);
 255    let start = movement::find_preceding_boundary_display_point(
 256        map,
 257        right(map, relative_to, 1),
 258        movement::FindRange::SingleLine,
 259        |left, right| classifier.kind(left) != classifier.kind(right),
 260    );
 261
 262    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
 263        classifier.kind(left) != classifier.kind(right)
 264    });
 265
 266    Some(start..end)
 267}
 268
 269pub fn surrounding_html_tag(
 270    map: &DisplaySnapshot,
 271    head: DisplayPoint,
 272    range: Range<DisplayPoint>,
 273    around: bool,
 274) -> Option<Range<DisplayPoint>> {
 275    fn read_tag(chars: impl Iterator<Item = char>) -> String {
 276        chars
 277            .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
 278            .collect()
 279    }
 280    fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 281        if Some('<') != chars.next() {
 282            return None;
 283        }
 284        Some(read_tag(chars))
 285    }
 286    fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
 287        if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
 288            return None;
 289        }
 290        Some(read_tag(chars))
 291    }
 292
 293    let snapshot = &map.buffer_snapshot;
 294    let offset = head.to_offset(map, Bias::Left);
 295    let excerpt = snapshot.excerpt_containing(offset..offset)?;
 296    let buffer = excerpt.buffer();
 297    let offset = excerpt.map_offset_to_buffer(offset);
 298
 299    // Find the most closest to current offset
 300    let mut cursor = buffer.syntax_layer_at(offset)?.node().walk();
 301    let mut last_child_node = cursor.node();
 302    while cursor.goto_first_child_for_byte(offset).is_some() {
 303        last_child_node = cursor.node();
 304    }
 305
 306    let mut last_child_node = Some(last_child_node);
 307    while let Some(cur_node) = last_child_node {
 308        if cur_node.child_count() >= 2 {
 309            let first_child = cur_node.child(0);
 310            let last_child = cur_node.child(cur_node.child_count() - 1);
 311            if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
 312                let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
 313                let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
 314                // It needs to be handled differently according to the selection length
 315                let is_valid = if range.end.to_offset(map, Bias::Left)
 316                    - range.start.to_offset(map, Bias::Left)
 317                    <= 1
 318                {
 319                    offset <= last_child.end_byte()
 320                } else {
 321                    range.start.to_offset(map, Bias::Left) >= first_child.start_byte()
 322                        && range.end.to_offset(map, Bias::Left) <= last_child.start_byte() + 1
 323                };
 324                if open_tag.is_some() && open_tag == close_tag && is_valid {
 325                    let range = if around {
 326                        first_child.byte_range().start..last_child.byte_range().end
 327                    } else {
 328                        first_child.byte_range().end..last_child.byte_range().start
 329                    };
 330                    if excerpt.contains_buffer_range(range.clone()) {
 331                        let result = excerpt.map_range_from_buffer(range);
 332                        return Some(
 333                            result.start.to_display_point(map)..result.end.to_display_point(map),
 334                        );
 335                    }
 336                }
 337            }
 338        }
 339        last_child_node = cur_node.parent();
 340    }
 341    None
 342}
 343
 344/// Returns a range that surrounds the word and following whitespace
 345/// relative_to is in.
 346///
 347/// If `relative_to` is at the start of a word, return the word and following whitespace.
 348/// If `relative_to` is between words, return the whitespace back and the following word.
 349///
 350/// if in word
 351///   delete that word
 352///   if there is whitespace following the word, delete that as well
 353///   otherwise, delete any preceding whitespace
 354/// otherwise
 355///   delete whitespace around cursor
 356///   delete word following the cursor
 357fn around_word(
 358    map: &DisplaySnapshot,
 359    relative_to: DisplayPoint,
 360    ignore_punctuation: bool,
 361) -> Option<Range<DisplayPoint>> {
 362    let offset = relative_to.to_offset(map, Bias::Left);
 363    let classifier = map
 364        .buffer_snapshot
 365        .char_classifier_at(offset)
 366        .ignore_punctuation(ignore_punctuation);
 367    let in_word = map
 368        .buffer_chars_at(offset)
 369        .next()
 370        .map(|(c, _)| !classifier.is_whitespace(c))
 371        .unwrap_or(false);
 372
 373    if in_word {
 374        around_containing_word(map, relative_to, ignore_punctuation)
 375    } else {
 376        around_next_word(map, relative_to, ignore_punctuation)
 377    }
 378}
 379
 380fn around_containing_word(
 381    map: &DisplaySnapshot,
 382    relative_to: DisplayPoint,
 383    ignore_punctuation: bool,
 384) -> Option<Range<DisplayPoint>> {
 385    in_word(map, relative_to, ignore_punctuation)
 386        .map(|range| expand_to_include_whitespace(map, range, true))
 387}
 388
 389fn around_next_word(
 390    map: &DisplaySnapshot,
 391    relative_to: DisplayPoint,
 392    ignore_punctuation: bool,
 393) -> Option<Range<DisplayPoint>> {
 394    let classifier = map
 395        .buffer_snapshot
 396        .char_classifier_at(relative_to.to_point(map))
 397        .ignore_punctuation(ignore_punctuation);
 398    // Get the start of the word
 399    let start = movement::find_preceding_boundary_display_point(
 400        map,
 401        right(map, relative_to, 1),
 402        FindRange::SingleLine,
 403        |left, right| classifier.kind(left) != classifier.kind(right),
 404    );
 405
 406    let mut word_found = false;
 407    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
 408        let left_kind = classifier.kind(left);
 409        let right_kind = classifier.kind(right);
 410
 411        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
 412
 413        if right_kind != CharKind::Whitespace {
 414            word_found = true;
 415        }
 416
 417        found
 418    });
 419
 420    Some(start..end)
 421}
 422
 423fn argument(
 424    map: &DisplaySnapshot,
 425    relative_to: DisplayPoint,
 426    around: bool,
 427) -> Option<Range<DisplayPoint>> {
 428    let snapshot = &map.buffer_snapshot;
 429    let offset = relative_to.to_offset(map, Bias::Left);
 430
 431    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
 432    let excerpt = snapshot.excerpt_containing(offset..offset)?;
 433    let buffer = excerpt.buffer();
 434
 435    fn comma_delimited_range_at(
 436        buffer: &BufferSnapshot,
 437        mut offset: usize,
 438        include_comma: bool,
 439    ) -> Option<Range<usize>> {
 440        // Seek to the first non-whitespace character
 441        offset += buffer
 442            .chars_at(offset)
 443            .take_while(|c| c.is_whitespace())
 444            .map(char::len_utf8)
 445            .sum::<usize>();
 446
 447        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
 448            // Filter out empty ranges
 449            if open.end == close.start {
 450                return false;
 451            }
 452
 453            // If the cursor is outside the brackets, ignore them
 454            if open.start == offset || close.end == offset {
 455                return false;
 456            }
 457
 458            // TODO: Is there any better way to filter out string brackets?
 459            // Used to filter out string brackets
 460            matches!(
 461                buffer.chars_at(open.start).next(),
 462                Some('(' | '[' | '{' | '<' | '|')
 463            )
 464        };
 465
 466        // Find the brackets containing the cursor
 467        let (open_bracket, close_bracket) =
 468            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
 469
 470        let inner_bracket_range = open_bracket.end..close_bracket.start;
 471
 472        let layer = buffer.syntax_layer_at(offset)?;
 473        let node = layer.node();
 474        let mut cursor = node.walk();
 475
 476        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
 477        let mut parent_covers_bracket_range = false;
 478        loop {
 479            let node = cursor.node();
 480            let range = node.byte_range();
 481            let covers_bracket_range =
 482                range.start == open_bracket.start && range.end == close_bracket.end;
 483            if parent_covers_bracket_range && !covers_bracket_range {
 484                break;
 485            }
 486            parent_covers_bracket_range = covers_bracket_range;
 487
 488            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
 489            cursor.goto_first_child_for_byte(offset)?;
 490        }
 491
 492        let mut argument_node = cursor.node();
 493
 494        // If the child node is the open bracket, move to the next sibling.
 495        if argument_node.byte_range() == open_bracket {
 496            if !cursor.goto_next_sibling() {
 497                return Some(inner_bracket_range);
 498            }
 499            argument_node = cursor.node();
 500        }
 501        // While the child node is the close bracket or a comma, move to the previous sibling
 502        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
 503            if !cursor.goto_previous_sibling() {
 504                return Some(inner_bracket_range);
 505            }
 506            argument_node = cursor.node();
 507            if argument_node.byte_range() == open_bracket {
 508                return Some(inner_bracket_range);
 509            }
 510        }
 511
 512        // The start and end of the argument range, defaulting to the start and end of the argument node
 513        let mut start = argument_node.start_byte();
 514        let mut end = argument_node.end_byte();
 515
 516        let mut needs_surrounding_comma = include_comma;
 517
 518        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
 519        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
 520        while cursor.goto_previous_sibling() {
 521            let prev = cursor.node();
 522
 523            if prev.start_byte() < open_bracket.end {
 524                start = open_bracket.end;
 525                break;
 526            } else if prev.kind() == "," {
 527                if needs_surrounding_comma {
 528                    start = prev.start_byte();
 529                    needs_surrounding_comma = false;
 530                }
 531                break;
 532            } else if prev.start_byte() < start {
 533                start = prev.start_byte();
 534            }
 535        }
 536
 537        // Do the same for the end of the argument, extending to next comma or the end of the argument list
 538        while cursor.goto_next_sibling() {
 539            let next = cursor.node();
 540
 541            if next.end_byte() > close_bracket.start {
 542                end = close_bracket.start;
 543                break;
 544            } else if next.kind() == "," {
 545                if needs_surrounding_comma {
 546                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
 547                    if let Some(next_arg) = next.next_sibling() {
 548                        end = next_arg.start_byte();
 549                    } else {
 550                        end = next.end_byte();
 551                    }
 552                }
 553                break;
 554            } else if next.end_byte() > end {
 555                end = next.end_byte();
 556            }
 557        }
 558
 559        Some(start..end)
 560    }
 561
 562    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
 563
 564    if excerpt.contains_buffer_range(result.clone()) {
 565        let result = excerpt.map_range_from_buffer(result);
 566        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
 567    } else {
 568        None
 569    }
 570}
 571
 572fn sentence(
 573    map: &DisplaySnapshot,
 574    relative_to: DisplayPoint,
 575    around: bool,
 576) -> Option<Range<DisplayPoint>> {
 577    let mut start = None;
 578    let relative_offset = relative_to.to_offset(map, Bias::Left);
 579    let mut previous_end = relative_offset;
 580
 581    let mut chars = map.buffer_chars_at(previous_end).peekable();
 582
 583    // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
 584    for (char, offset) in chars
 585        .peek()
 586        .cloned()
 587        .into_iter()
 588        .chain(map.reverse_buffer_chars_at(previous_end))
 589    {
 590        if is_sentence_end(map, offset) {
 591            break;
 592        }
 593
 594        if is_possible_sentence_start(char) {
 595            start = Some(offset);
 596        }
 597
 598        previous_end = offset;
 599    }
 600
 601    // Search forward for the end of the current sentence or if we are between sentences, the start of the next one
 602    let mut end = relative_offset;
 603    for (char, offset) in chars {
 604        if start.is_none() && is_possible_sentence_start(char) {
 605            if around {
 606                start = Some(offset);
 607                continue;
 608            } else {
 609                end = offset;
 610                break;
 611            }
 612        }
 613
 614        if char != '\n' {
 615            end = offset + char.len_utf8();
 616        }
 617
 618        if is_sentence_end(map, end) {
 619            break;
 620        }
 621    }
 622
 623    let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
 624    if around {
 625        range = expand_to_include_whitespace(map, range, false);
 626    }
 627
 628    Some(range)
 629}
 630
 631fn is_possible_sentence_start(character: char) -> bool {
 632    !character.is_whitespace() && character != '.'
 633}
 634
 635const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
 636const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
 637const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
 638fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
 639    let mut next_chars = map.buffer_chars_at(offset).peekable();
 640    if let Some((char, _)) = next_chars.next() {
 641        // We are at a double newline. This position is a sentence end.
 642        if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
 643            return true;
 644        }
 645
 646        // The next text is not a valid whitespace. This is not a sentence end
 647        if !SENTENCE_END_WHITESPACE.contains(&char) {
 648            return false;
 649        }
 650    }
 651
 652    for (char, _) in map.reverse_buffer_chars_at(offset) {
 653        if SENTENCE_END_PUNCTUATION.contains(&char) {
 654            return true;
 655        }
 656
 657        if !SENTENCE_END_FILLERS.contains(&char) {
 658            return false;
 659        }
 660    }
 661
 662    false
 663}
 664
 665/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
 666/// whitespace to the end first and falls back to the start if there was none.
 667fn expand_to_include_whitespace(
 668    map: &DisplaySnapshot,
 669    range: Range<DisplayPoint>,
 670    stop_at_newline: bool,
 671) -> Range<DisplayPoint> {
 672    let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
 673    let mut whitespace_included = false;
 674
 675    let chars = map.buffer_chars_at(range.end).peekable();
 676    for (char, offset) in chars {
 677        if char == '\n' && stop_at_newline {
 678            break;
 679        }
 680
 681        if char.is_whitespace() {
 682            if char != '\n' {
 683                range.end = offset + char.len_utf8();
 684                whitespace_included = true;
 685            }
 686        } else {
 687            // Found non whitespace. Quit out.
 688            break;
 689        }
 690    }
 691
 692    if !whitespace_included {
 693        for (char, point) in map.reverse_buffer_chars_at(range.start) {
 694            if char == '\n' && stop_at_newline {
 695                break;
 696            }
 697
 698            if !char.is_whitespace() {
 699                break;
 700            }
 701
 702            range.start = point;
 703        }
 704    }
 705
 706    range.start.to_display_point(map)..range.end.to_display_point(map)
 707}
 708
 709/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
 710/// where `relative_to` is in. If `around`, principally returns the range ending
 711/// at the end of the next paragraph.
 712///
 713/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
 714/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
 715/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
 716/// the trailing newline is not subject to subsequent operations).
 717///
 718/// Edge cases:
 719/// - If `around` and if the current paragraph is the last paragraph of the
 720///   file and is blank, then the selection results in an error.
 721/// - If `around` and if the current paragraph is the last paragraph of the
 722///   file and is not blank, then the returned range starts at the start of the
 723///   previous paragraph, if it exists.
 724fn paragraph(
 725    map: &DisplaySnapshot,
 726    relative_to: DisplayPoint,
 727    around: bool,
 728) -> Option<Range<DisplayPoint>> {
 729    let mut paragraph_start = start_of_paragraph(map, relative_to);
 730    let mut paragraph_end = end_of_paragraph(map, relative_to);
 731
 732    let paragraph_end_row = paragraph_end.row();
 733    let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
 734    let point = relative_to.to_point(map);
 735    let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 736
 737    if around {
 738        if paragraph_ends_with_eof {
 739            if current_line_is_empty {
 740                return None;
 741            }
 742
 743            let paragraph_start_row = paragraph_start.row();
 744            if paragraph_start_row.0 != 0 {
 745                let previous_paragraph_last_line_start =
 746                    DisplayPoint::new(paragraph_start_row - 1, 0);
 747                paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
 748            }
 749        } else {
 750            let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
 751            paragraph_end = end_of_paragraph(map, next_paragraph_start);
 752        }
 753    }
 754
 755    let range = paragraph_start..paragraph_end;
 756    Some(range)
 757}
 758
 759/// Returns a position of the start of the current paragraph, where a paragraph
 760/// is defined as a run of non-blank lines or a run of blank lines.
 761pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 762    let point = display_point.to_point(map);
 763    if point.row == 0 {
 764        return DisplayPoint::zero();
 765    }
 766
 767    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 768
 769    for row in (0..point.row).rev() {
 770        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
 771        if blank != is_current_line_blank {
 772            return Point::new(row + 1, 0).to_display_point(map);
 773        }
 774    }
 775
 776    DisplayPoint::zero()
 777}
 778
 779/// Returns a position of the end of the current paragraph, where a paragraph
 780/// is defined as a run of non-blank lines or a run of blank lines.
 781/// The trailing newline is excluded from the paragraph.
 782pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 783    let point = display_point.to_point(map);
 784    if point.row == map.max_buffer_row().0 {
 785        return map.max_point();
 786    }
 787
 788    let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
 789
 790    for row in point.row + 1..map.max_buffer_row().0 + 1 {
 791        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
 792        if blank != is_current_line_blank {
 793            let previous_row = row - 1;
 794            return Point::new(
 795                previous_row,
 796                map.buffer_snapshot.line_len(MultiBufferRow(previous_row)),
 797            )
 798            .to_display_point(map);
 799        }
 800    }
 801
 802    map.max_point()
 803}
 804
 805fn surrounding_markers(
 806    map: &DisplaySnapshot,
 807    relative_to: DisplayPoint,
 808    around: bool,
 809    search_across_lines: bool,
 810    open_marker: char,
 811    close_marker: char,
 812) -> Option<Range<DisplayPoint>> {
 813    let point = relative_to.to_offset(map, Bias::Left);
 814
 815    let mut matched_closes = 0;
 816    let mut opening = None;
 817
 818    let mut before_ch = match movement::chars_before(map, point).next() {
 819        Some((ch, _)) => ch,
 820        _ => '\0',
 821    };
 822    if let Some((ch, range)) = movement::chars_after(map, point).next() {
 823        if ch == open_marker && before_ch != '\\' {
 824            if open_marker == close_marker {
 825                let mut total = 0;
 826                for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
 827                {
 828                    if ch == '\n' {
 829                        break;
 830                    }
 831                    if ch == open_marker && before_ch != '\\' {
 832                        total += 1;
 833                    }
 834                }
 835                if total % 2 == 0 {
 836                    opening = Some(range)
 837                }
 838            } else {
 839                opening = Some(range)
 840            }
 841        }
 842    }
 843
 844    if opening.is_none() {
 845        let mut chars_before = movement::chars_before(map, point).peekable();
 846        while let Some((ch, range)) = chars_before.next() {
 847            if ch == '\n' && !search_across_lines {
 848                break;
 849            }
 850
 851            if let Some((before_ch, _)) = chars_before.peek() {
 852                if *before_ch == '\\' {
 853                    continue;
 854                }
 855            }
 856
 857            if ch == open_marker {
 858                if matched_closes == 0 {
 859                    opening = Some(range);
 860                    break;
 861                }
 862                matched_closes -= 1;
 863            } else if ch == close_marker {
 864                matched_closes += 1
 865            }
 866        }
 867    }
 868    if opening.is_none() {
 869        for (ch, range) in movement::chars_after(map, point) {
 870            if before_ch != '\\' {
 871                if ch == open_marker {
 872                    opening = Some(range);
 873                    break;
 874                } else if ch == close_marker {
 875                    break;
 876                }
 877            }
 878
 879            before_ch = ch;
 880        }
 881    }
 882
 883    let mut opening = opening?;
 884
 885    let mut matched_opens = 0;
 886    let mut closing = None;
 887    before_ch = match movement::chars_before(map, opening.end).next() {
 888        Some((ch, _)) => ch,
 889        _ => '\0',
 890    };
 891    for (ch, range) in movement::chars_after(map, opening.end) {
 892        if ch == '\n' && !search_across_lines {
 893            break;
 894        }
 895
 896        if before_ch != '\\' {
 897            if ch == close_marker {
 898                if matched_opens == 0 {
 899                    closing = Some(range);
 900                    break;
 901                }
 902                matched_opens -= 1;
 903            } else if ch == open_marker {
 904                matched_opens += 1;
 905            }
 906        }
 907
 908        before_ch = ch;
 909    }
 910
 911    let mut closing = closing?;
 912
 913    if around && !search_across_lines {
 914        let mut found = false;
 915
 916        for (ch, range) in movement::chars_after(map, closing.end) {
 917            if ch.is_whitespace() && ch != '\n' {
 918                found = true;
 919                closing.end = range.end;
 920            } else {
 921                break;
 922            }
 923        }
 924
 925        if !found {
 926            for (ch, range) in movement::chars_before(map, opening.start) {
 927                if ch.is_whitespace() && ch != '\n' {
 928                    opening.start = range.start
 929                } else {
 930                    break;
 931                }
 932            }
 933        }
 934    }
 935
 936    if !around && search_across_lines {
 937        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
 938            if ch == '\n' {
 939                opening.end = range.end
 940            }
 941        }
 942
 943        for (ch, range) in movement::chars_before(map, closing.start) {
 944            if !ch.is_whitespace() {
 945                break;
 946            }
 947            if ch != '\n' {
 948                closing.start = range.start
 949            }
 950        }
 951    }
 952
 953    let result = if around {
 954        opening.start..closing.end
 955    } else {
 956        opening.end..closing.start
 957    };
 958
 959    Some(
 960        map.clip_point(result.start.to_display_point(map), Bias::Left)
 961            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
 962    )
 963}
 964
 965#[cfg(test)]
 966mod test {
 967    use indoc::indoc;
 968
 969    use crate::{
 970        state::Mode,
 971        test::{NeovimBackedTestContext, VimTestContext},
 972    };
 973
 974    const WORD_LOCATIONS: &str = indoc! {"
 975        The quick ˇbrowˇnˇ•••
 976        fox ˇjuˇmpsˇ over
 977        the lazy dogˇ••
 978        ˇ
 979        ˇ
 980        ˇ
 981        Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
 982        ˇ••
 983        ˇ••
 984        ˇ  fox-jumpˇs over
 985        the lazy dogˇ•
 986        ˇ
 987        "
 988    };
 989
 990    #[gpui::test]
 991    async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
 992        let mut cx = NeovimBackedTestContext::new(cx).await;
 993
 994        cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
 995            .await
 996            .assert_matches();
 997        cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
 998            .await
 999            .assert_matches();
1000        cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
1001            .await
1002            .assert_matches();
1003        cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
1004            .await
1005            .assert_matches();
1006    }
1007
1008    #[gpui::test]
1009    async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
1010        let mut cx = NeovimBackedTestContext::new(cx).await;
1011
1012        cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
1013            .await
1014            .assert_matches();
1015        cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
1016            .await
1017            .assert_matches();
1018        cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
1019            .await
1020            .assert_matches();
1021        cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
1022            .await
1023            .assert_matches();
1024    }
1025
1026    #[gpui::test]
1027    async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
1028        let mut cx = NeovimBackedTestContext::new(cx).await;
1029
1030        /*
1031                cx.set_shared_state("The quick ˇbrown\nfox").await;
1032                cx.simulate_shared_keystrokes(["v"]).await;
1033                cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
1034                cx.simulate_shared_keystrokes(["i", "w"]).await;
1035                cx.assert_shared_state("The quick «brownˇ»\nfox").await;
1036        */
1037        cx.set_shared_state("The quick brown\nˇ\nfox").await;
1038        cx.simulate_shared_keystrokes("v").await;
1039        cx.shared_state()
1040            .await
1041            .assert_eq("The quick brown\n«\nˇ»fox");
1042        cx.simulate_shared_keystrokes("i w").await;
1043        cx.shared_state()
1044            .await
1045            .assert_eq("The quick brown\n«\nˇ»fox");
1046
1047        cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
1048            .await
1049            .assert_matches();
1050        cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
1051            .await
1052            .assert_matches();
1053    }
1054
1055    const PARAGRAPH_EXAMPLES: &[&str] = &[
1056        // Single line
1057        "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
1058        // Multiple lines without empty lines
1059        indoc! {"
1060            ˇThe quick brownˇ
1061            ˇfox jumps overˇ
1062            the lazy dog.ˇ
1063        "},
1064        // Heading blank paragraph and trailing normal paragraph
1065        indoc! {"
1066            ˇ
1067            ˇ
1068            ˇThe quick brown fox jumps
1069            ˇover the lazy dog.
1070            ˇ
1071            ˇ
1072            ˇThe quick brown fox jumpsˇ
1073            ˇover the lazy dog.ˇ
1074        "},
1075        // Inserted blank paragraph and trailing blank paragraph
1076        indoc! {"
1077            ˇThe quick brown fox jumps
1078            ˇover the lazy dog.
1079            ˇ
1080            ˇ
1081            ˇ
1082            ˇThe quick brown fox jumpsˇ
1083            ˇover the lazy dog.ˇ
1084            ˇ
1085            ˇ
1086            ˇ
1087        "},
1088        // "Blank" paragraph with whitespace characters
1089        indoc! {"
1090            ˇThe quick brown fox jumps
1091            over the lazy dog.
1092
1093            ˇ \t
1094
1095            ˇThe quick brown fox jumps
1096            over the lazy dog.ˇ
1097            ˇ
1098            ˇ \t
1099            \t \t
1100        "},
1101        // Single line "paragraphs", where selection size might be zero.
1102        indoc! {"
1103            ˇThe quick brown fox jumps over the lazy dog.
1104            ˇ
1105            ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
1106            ˇ
1107        "},
1108    ];
1109
1110    #[gpui::test]
1111    async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
1112        let mut cx = NeovimBackedTestContext::new(cx).await;
1113
1114        for paragraph_example in PARAGRAPH_EXAMPLES {
1115            cx.simulate_at_each_offset("c i p", paragraph_example)
1116                .await
1117                .assert_matches();
1118            cx.simulate_at_each_offset("c a p", paragraph_example)
1119                .await
1120                .assert_matches();
1121        }
1122    }
1123
1124    #[gpui::test]
1125    async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
1126        let mut cx = NeovimBackedTestContext::new(cx).await;
1127
1128        for paragraph_example in PARAGRAPH_EXAMPLES {
1129            cx.simulate_at_each_offset("d i p", paragraph_example)
1130                .await
1131                .assert_matches();
1132            cx.simulate_at_each_offset("d a p", paragraph_example)
1133                .await
1134                .assert_matches();
1135        }
1136    }
1137
1138    #[gpui::test]
1139    async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
1140        let mut cx = NeovimBackedTestContext::new(cx).await;
1141
1142        const EXAMPLES: &[&str] = &[
1143            indoc! {"
1144                ˇThe quick brown
1145                fox jumps over
1146                the lazy dog.
1147            "},
1148            indoc! {"
1149                ˇ
1150
1151                ˇThe quick brown fox jumps
1152                over the lazy dog.
1153                ˇ
1154
1155                ˇThe quick brown fox jumps
1156                over the lazy dog.
1157            "},
1158            indoc! {"
1159                ˇThe quick brown fox jumps over the lazy dog.
1160                ˇ
1161                ˇThe quick brown fox jumps over the lazy dog.
1162
1163            "},
1164        ];
1165
1166        for paragraph_example in EXAMPLES {
1167            cx.simulate_at_each_offset("v i p", paragraph_example)
1168                .await
1169                .assert_matches();
1170            cx.simulate_at_each_offset("v a p", paragraph_example)
1171                .await
1172                .assert_matches();
1173        }
1174    }
1175
1176    // Test string with "`" for opening surrounders and "'" for closing surrounders
1177    const SURROUNDING_MARKER_STRING: &str = indoc! {"
1178        ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
1179        'ˇfox juˇmps ov`ˇer
1180        the ˇlazy d'o`ˇg"};
1181
1182    const SURROUNDING_OBJECTS: &[(char, char)] = &[
1183        ('"', '"'), // Double Quote
1184        ('(', ')'), // Parentheses
1185    ];
1186
1187    #[gpui::test]
1188    async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1189        let mut cx = NeovimBackedTestContext::new(cx).await;
1190
1191        for (start, end) in SURROUNDING_OBJECTS {
1192            let marked_string = SURROUNDING_MARKER_STRING
1193                .replace('`', &start.to_string())
1194                .replace('\'', &end.to_string());
1195
1196            cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
1197                .await
1198                .assert_matches();
1199            cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
1200                .await
1201                .assert_matches();
1202            cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
1203                .await
1204                .assert_matches();
1205            cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
1206                .await
1207                .assert_matches();
1208        }
1209    }
1210    #[gpui::test]
1211    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1212        let mut cx = NeovimBackedTestContext::new(cx).await;
1213        cx.set_shared_wrap(12).await;
1214
1215        cx.set_shared_state(indoc! {
1216            "\"ˇhello world\"!"
1217        })
1218        .await;
1219        cx.simulate_shared_keystrokes("v i \"").await;
1220        cx.shared_state().await.assert_eq(indoc! {
1221            "\"«hello worldˇ»\"!"
1222        });
1223
1224        cx.set_shared_state(indoc! {
1225            "\"hˇello world\"!"
1226        })
1227        .await;
1228        cx.simulate_shared_keystrokes("v i \"").await;
1229        cx.shared_state().await.assert_eq(indoc! {
1230            "\"«hello worldˇ»\"!"
1231        });
1232
1233        cx.set_shared_state(indoc! {
1234            "helˇlo \"world\"!"
1235        })
1236        .await;
1237        cx.simulate_shared_keystrokes("v i \"").await;
1238        cx.shared_state().await.assert_eq(indoc! {
1239            "hello \"«worldˇ»\"!"
1240        });
1241
1242        cx.set_shared_state(indoc! {
1243            "hello \"wˇorld\"!"
1244        })
1245        .await;
1246        cx.simulate_shared_keystrokes("v i \"").await;
1247        cx.shared_state().await.assert_eq(indoc! {
1248            "hello \"«worldˇ»\"!"
1249        });
1250
1251        cx.set_shared_state(indoc! {
1252            "hello \"wˇorld\"!"
1253        })
1254        .await;
1255        cx.simulate_shared_keystrokes("v a \"").await;
1256        cx.shared_state().await.assert_eq(indoc! {
1257            "hello« \"world\"ˇ»!"
1258        });
1259
1260        cx.set_shared_state(indoc! {
1261            "hello \"wˇorld\" !"
1262        })
1263        .await;
1264        cx.simulate_shared_keystrokes("v a \"").await;
1265        cx.shared_state().await.assert_eq(indoc! {
1266            "hello «\"world\" ˇ»!"
1267        });
1268
1269        cx.set_shared_state(indoc! {
1270            "hello \"wˇorld\"1271            goodbye"
1272        })
1273        .await;
1274        cx.simulate_shared_keystrokes("v a \"").await;
1275        cx.shared_state().await.assert_eq(indoc! {
1276            "hello «\"world\" ˇ»
1277            goodbye"
1278        });
1279    }
1280
1281    #[gpui::test]
1282    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1283        let mut cx = NeovimBackedTestContext::new(cx).await;
1284
1285        cx.set_shared_state(indoc! {
1286            "func empty(a string) bool {
1287               if a == \"\" {
1288                  return true
1289               }
1290               ˇreturn false
1291            }"
1292        })
1293        .await;
1294        cx.simulate_shared_keystrokes("v i {").await;
1295        cx.shared_state().await.assert_eq(indoc! {"
1296            func empty(a string) bool {
1297            «   if a == \"\" {
1298                  return true
1299               }
1300               return false
1301            ˇ»}"});
1302        cx.set_shared_state(indoc! {
1303            "func empty(a string) bool {
1304                 if a == \"\" {
1305                     ˇreturn true
1306                 }
1307                 return false
1308            }"
1309        })
1310        .await;
1311        cx.simulate_shared_keystrokes("v i {").await;
1312        cx.shared_state().await.assert_eq(indoc! {"
1313            func empty(a string) bool {
1314                 if a == \"\" {
1315            «         return true
1316            ˇ»     }
1317                 return false
1318            }"});
1319
1320        cx.set_shared_state(indoc! {
1321            "func empty(a string) bool {
1322                 if a == \"\" ˇ{
1323                     return true
1324                 }
1325                 return false
1326            }"
1327        })
1328        .await;
1329        cx.simulate_shared_keystrokes("v i {").await;
1330        cx.shared_state().await.assert_eq(indoc! {"
1331            func empty(a string) bool {
1332                 if a == \"\" {
1333            «         return true
1334            ˇ»     }
1335                 return false
1336            }"});
1337    }
1338
1339    #[gpui::test]
1340    async fn test_singleline_surrounding_character_objects_with_escape(
1341        cx: &mut gpui::TestAppContext,
1342    ) {
1343        let mut cx = NeovimBackedTestContext::new(cx).await;
1344        cx.set_shared_state(indoc! {
1345            "h\"e\\\"lˇlo \\\"world\"!"
1346        })
1347        .await;
1348        cx.simulate_shared_keystrokes("v i \"").await;
1349        cx.shared_state().await.assert_eq(indoc! {
1350            "h\"«e\\\"llo \\\"worldˇ»\"!"
1351        });
1352
1353        cx.set_shared_state(indoc! {
1354            "hello \"teˇst \\\"inside\\\" world\""
1355        })
1356        .await;
1357        cx.simulate_shared_keystrokes("v i \"").await;
1358        cx.shared_state().await.assert_eq(indoc! {
1359            "hello \"«test \\\"inside\\\" worldˇ»\""
1360        });
1361    }
1362
1363    #[gpui::test]
1364    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
1365        let mut cx = VimTestContext::new(cx, true).await;
1366        cx.set_state(
1367            indoc! {"
1368            fn boop() {
1369                baz(ˇ|a, b| { bar(|j, k| { })})
1370            }"
1371            },
1372            Mode::Normal,
1373        );
1374        cx.simulate_keystrokes("c i |");
1375        cx.assert_state(
1376            indoc! {"
1377            fn boop() {
1378                baz(|ˇ| { bar(|j, k| { })})
1379            }"
1380            },
1381            Mode::Insert,
1382        );
1383        cx.simulate_keystrokes("escape 1 8 |");
1384        cx.assert_state(
1385            indoc! {"
1386            fn boop() {
1387                baz(|| { bar(ˇ|j, k| { })})
1388            }"
1389            },
1390            Mode::Normal,
1391        );
1392
1393        cx.simulate_keystrokes("v a |");
1394        cx.assert_state(
1395            indoc! {"
1396            fn boop() {
1397                baz(|| { bar(«|j, k| ˇ»{ })})
1398            }"
1399            },
1400            Mode::Visual,
1401        );
1402    }
1403
1404    #[gpui::test]
1405    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
1406        let mut cx = VimTestContext::new(cx, true).await;
1407
1408        // Generic arguments
1409        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
1410        cx.simulate_keystrokes("v i g");
1411        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
1412
1413        // Function arguments
1414        cx.set_state(
1415            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
1416            Mode::Normal,
1417        );
1418        cx.simulate_keystrokes("d a g");
1419        cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
1420
1421        cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
1422        cx.simulate_keystrokes("v a g");
1423        cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
1424
1425        // Tuple, vec, and array arguments
1426        cx.set_state(
1427            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
1428            Mode::Normal,
1429        );
1430        cx.simulate_keystrokes("c i g");
1431        cx.assert_state(
1432            "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
1433            Mode::Insert,
1434        );
1435
1436        cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
1437        cx.simulate_keystrokes("c a g");
1438        cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
1439
1440        cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
1441        cx.simulate_keystrokes("c i g");
1442        cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
1443
1444        cx.set_state(
1445            "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
1446            Mode::Normal,
1447        );
1448        cx.simulate_keystrokes("c a g");
1449        cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
1450
1451        // Cursor immediately before / after brackets
1452        cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
1453        cx.simulate_keystrokes("v i g");
1454        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1455
1456        cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
1457        cx.simulate_keystrokes("v i g");
1458        cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
1459    }
1460
1461    #[gpui::test]
1462    async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
1463        let mut cx = NeovimBackedTestContext::new(cx).await;
1464
1465        for (start, end) in SURROUNDING_OBJECTS {
1466            let marked_string = SURROUNDING_MARKER_STRING
1467                .replace('`', &start.to_string())
1468                .replace('\'', &end.to_string());
1469
1470            cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
1471                .await
1472                .assert_matches();
1473            cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
1474                .await
1475                .assert_matches();
1476            cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
1477                .await
1478                .assert_matches();
1479            cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
1480                .await
1481                .assert_matches();
1482        }
1483    }
1484
1485    #[gpui::test]
1486    async fn test_tags(cx: &mut gpui::TestAppContext) {
1487        let mut cx = VimTestContext::new_html(cx).await;
1488
1489        cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
1490        cx.simulate_keystrokes("v i t");
1491        cx.assert_state(
1492            "<html><head></head><body><b>«hi!ˇ»</b></body>",
1493            Mode::Visual,
1494        );
1495        cx.simulate_keystrokes("a t");
1496        cx.assert_state(
1497            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1498            Mode::Visual,
1499        );
1500        cx.simulate_keystrokes("a t");
1501        cx.assert_state(
1502            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1503            Mode::Visual,
1504        );
1505
1506        // The cursor is before the tag
1507        cx.set_state(
1508            "<html><head></head><body> ˇ  <b>hi!</b></body>",
1509            Mode::Normal,
1510        );
1511        cx.simulate_keystrokes("v i t");
1512        cx.assert_state(
1513            "<html><head></head><body>   <b>«hi!ˇ»</b></body>",
1514            Mode::Visual,
1515        );
1516        cx.simulate_keystrokes("a t");
1517        cx.assert_state(
1518            "<html><head></head><body>   «<b>hi!</b>ˇ»</body>",
1519            Mode::Visual,
1520        );
1521
1522        // The cursor is in the open tag
1523        cx.set_state(
1524            "<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
1525            Mode::Normal,
1526        );
1527        cx.simulate_keystrokes("v a t");
1528        cx.assert_state(
1529            "<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
1530            Mode::Visual,
1531        );
1532        cx.simulate_keystrokes("i t");
1533        cx.assert_state(
1534            "<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
1535            Mode::Visual,
1536        );
1537
1538        // current selection length greater than 1
1539        cx.set_state(
1540            "<html><head></head><body><«b>hi!ˇ»</b></body>",
1541            Mode::Visual,
1542        );
1543        cx.simulate_keystrokes("i t");
1544        cx.assert_state(
1545            "<html><head></head><body><b>«hi!ˇ»</b></body>",
1546            Mode::Visual,
1547        );
1548        cx.simulate_keystrokes("a t");
1549        cx.assert_state(
1550            "<html><head></head><body>«<b>hi!</b>ˇ»</body>",
1551            Mode::Visual,
1552        );
1553
1554        cx.set_state(
1555            "<html><head></head><body><«b>hi!</ˇ»b></body>",
1556            Mode::Visual,
1557        );
1558        cx.simulate_keystrokes("a t");
1559        cx.assert_state(
1560            "<html><head></head>«<body><b>hi!</b></body>ˇ»",
1561            Mode::Visual,
1562        );
1563    }
1564}